state
About 1087 wordsAbout 4 min
2025-08-28
This guide covers state management in the React chatbot application using React hooks and custom state management patterns.
Overview
The chatbot application uses React hooks for state management, providing a simple and efficient way to handle application state without external libraries.
Core State Types
Chat State
// src/types/chat.ts
export interface Message {
id: string
content: string
role: 'user' | 'assistant'
timestamp: Date
}
export interface ChatState {
messages: Message[]
isLoading: boolean
error: string | null
isTyping: boolean
}
export interface ChatActions {
sendMessage: (content: string) => Promise<void>
clearMessages: () => void
setError: (error: string | null) => void
setIsLoading: (loading: boolean) => void
}Custom Hooks
useChat Hook
Main hook for managing chat state and interactions.
// src/hooks/useChat.ts
import { useState, useCallback, useRef } from 'react'
import { Message, ChatState, ChatActions } from '../types/chat'
import { useOpenAI } from './useOpenAI'
import { useLocalStorage } from './useLocalStorage'
export const useChat = (): ChatState & ChatActions => {
const [messages, setMessages] = useLocalStorage<Message[]>('chat-messages', [])
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [isTyping, setIsTyping] = useState(false)
const { sendMessageToOpenAI } = useOpenAI()
const abortControllerRef = useRef<AbortController | null>(null)
const sendMessage = useCallback(async (content: string) => {
if (!content.trim()) return
const userMessage: Message = {
id: Date.now().toString(),
content: content.trim(),
role: 'user',
timestamp: new Date()
}
setMessages(prev => [...prev, userMessage])
setIsLoading(true)
setError(null)
try {
abortControllerRef.current = new AbortController()
const response = await sendMessageToOpenAI(
content,
messages,
abortControllerRef.current.signal
)
const assistantMessage: Message = {
id: (Date.now() + 1).toString(),
content: response,
role: 'assistant',
timestamp: new Date()
}
setMessages(prev => [...prev, assistantMessage])
} catch (err) {
if (err instanceof Error && err.name === 'AbortError') {
return
}
setError(err instanceof Error ? err.message : 'Failed to send message')
} finally {
setIsLoading(false)
setIsTyping(false)
abortControllerRef.current = null
}
}, [messages, sendMessageToOpenAI, setMessages])
const clearMessages = useCallback(() => {
setMessages([])
setError(null)
}, [setMessages])
const stopGeneration = useCallback(() => {
if (abortControllerRef.current) {
abortControllerRef.current.abort()
setIsLoading(false)
setIsTyping(false)
}
}, [])
return {
messages,
isLoading,
error,
isTyping,
sendMessage,
clearMessages,
setError,
setIsLoading,
stopGeneration
}
}useOpenAI Hook
Hook for managing OpenAI API interactions.
// src/hooks/useOpenAI.ts
import { useCallback } from 'react'
import { Message } from '../types/chat'
import { openAIService } from '../services/openai'
export const useOpenAI = () => {
const sendMessageToOpenAI = useCallback(async (
content: string,
messages: Message[],
signal?: AbortSignal
): Promise<string> => {
const conversationHistory = messages.map(msg => ({
role: msg.role,
content: msg.content
}))
const response = await openAIService.sendMessage({
message: content,
history: conversationHistory,
signal
})
return response
}, [])
return {
sendMessageToOpenAI
}
}useLocalStorage Hook
Hook for persisting state in localStorage.
// src/hooks/useLocalStorage.ts
import { useState, useEffect } from 'react'
export function useLocalStorage<T>(
key: string,
initialValue: T
): [T, (value: T | ((val: T) => T)) => void] {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key)
return item ? JSON.parse(item) : initialValue
} catch (error) {
console.error(`Error reading localStorage key "${key}":`, error)
return initialValue
}
})
const setValue = (value: T | ((val: T) => T)) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value
setStoredValue(valueToStore)
window.localStorage.setItem(key, JSON.stringify(valueToStore))
} catch (error) {
console.error(`Error setting localStorage key "${key}":`, error)
}
}
useEffect(() => {
const handleStorageChange = (e: StorageEvent) => {
if (e.key === key && e.newValue !== null) {
try {
setStoredValue(JSON.parse(e.newValue))
} catch (error) {
console.error(`Error parsing localStorage value for key "${key}":`, error)
}
}
}
window.addEventListener('storage', handleStorageChange)
return () => window.removeEventListener('storage', handleStorageChange)
}, [key])
return [storedValue, setValue]
}useDebounce Hook
Hook for debouncing values, useful for search inputs.
// src/hooks/useDebounce.ts
import { useState, useEffect } from 'react'
export function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value)
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value)
}, delay)
return () => {
clearTimeout(handler)
}
}, [value, delay])
return debouncedValue
}useTheme Hook
Hook for managing application theme.
// src/hooks/useTheme.ts
import { useState, useEffect } from 'react'
import { useLocalStorage } from './useLocalStorage'
type Theme = 'light' | 'dark' | 'system'
export function useTheme() {
const [theme, setTheme] = useLocalStorage<Theme>('theme', 'system')
const [resolvedTheme, setResolvedTheme] = useState<'light' | 'dark'>('light')
useEffect(() => {
const updateResolvedTheme = () => {
if (theme === 'system') {
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light'
setResolvedTheme(systemTheme)
} else {
setResolvedTheme(theme)
}
}
updateResolvedTheme()
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
mediaQuery.addEventListener('change', updateResolvedTheme)
return () => mediaQuery.removeEventListener('change', updateResolvedTheme)
}, [theme])
useEffect(() => {
const root = window.document.documentElement
root.classList.remove('light', 'dark')
root.classList.add(resolvedTheme)
}, [resolvedTheme])
return {
theme,
setTheme,
resolvedTheme
}
}State Management Patterns
Context Pattern
For sharing state across components, use React Context:
// src/contexts/ChatContext.tsx
import React, { createContext, useContext, ReactNode } from 'react'
import { useChat } from '../hooks/useChat'
interface ChatContextType {
messages: Message[]
isLoading: boolean
error: string | null
sendMessage: (content: string) => Promise<void>
clearMessages: () => void
}
const ChatContext = createContext<ChatContextType | undefined>(undefined)
export const ChatProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const chatState = useChat()
return (
<ChatContext.Provider value={chatState}>
{children}
</ChatContext.Provider>
)
}
export const useChatContext = () => {
const context = useContext(ChatContext)
if (context === undefined) {
throw new Error('useChatContext must be used within a ChatProvider')
}
return context
}Reducer Pattern
For complex state logic, use useReducer:
// src/reducers/chatReducer.ts
import { Message } from '../types/chat'
export interface ChatState {
messages: Message[]
isLoading: boolean
error: string | null
isTyping: boolean
}
export type ChatAction =
| { type: 'SEND_MESSAGE'; payload: Message }
| { type: 'RECEIVE_MESSAGE'; payload: Message }
| { type: 'SET_LOADING'; payload: boolean }
| { type: 'SET_ERROR'; payload: string | null }
| { type: 'CLEAR_MESSAGES' }
| { type: 'SET_TYPING'; payload: boolean }
export const chatReducer = (state: ChatState, action: ChatAction): ChatState => {
switch (action.type) {
case 'SEND_MESSAGE':
return {
...state,
messages: [...state.messages, action.payload],
error: null
}
case 'RECEIVE_MESSAGE':
return {
...state,
messages: [...state.messages, action.payload],
isLoading: false,
isTyping: false
}
case 'SET_LOADING':
return {
...state,
isLoading: action.payload
}
case 'SET_ERROR':
return {
...state,
error: action.payload,
isLoading: false,
isTyping: false
}
case 'CLEAR_MESSAGES':
return {
...state,
messages: [],
error: null
}
case 'SET_TYPING':
return {
...state,
isTyping: action.payload
}
default:
return state
}
}Performance Optimization
Memoization
Use React.memo for expensive components:
// src/components/chat/MessageItem.tsx
import React from 'react'
import { Message } from '../../types/chat'
interface MessageItemProps {
message: Message
}
export const MessageItem = React.memo<MessageItemProps>(({ message }) => {
// Component implementation
})Callback Optimization
Use useCallback for stable function references:
const handleSendMessage = useCallback((content: string) => {
sendMessage(content)
}, [sendMessage])
const handleClearMessages = useCallback(() => {
clearMessages()
}, [clearMessages])Error Handling
Error Boundaries
Create error boundaries for component error handling:
// src/components/ErrorBoundary.tsx
import React, { Component, ErrorInfo, ReactNode } from 'react'
interface Props {
children: ReactNode
fallback?: ReactNode
}
interface State {
hasError: boolean
error?: Error
}
export class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props)
this.state = { hasError: false }
}
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error }
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('Error caught by boundary:', error, errorInfo)
}
render() {
if (this.state.hasError) {
return this.props.fallback || (
<div className="p-4 bg-red-50 border border-red-200 rounded-lg">
<h2 className="text-red-800 font-medium">Something went wrong</h2>
<p className="text-red-600 text-sm mt-1">
{this.state.error?.message}
</p>
</div>
)
}
return this.props.children
}
}Best Practices
- Single Source of Truth: Keep state in the highest common ancestor
- Immutable Updates: Always create new objects/arrays when updating state
- Local State: Use local state for component-specific data
- Persistence: Use localStorage for important state that should survive page reloads
- Error Handling: Implement proper error boundaries and error states
- Performance: Use memoization and optimization techniques when needed
- Type Safety: Use TypeScript for all state management
Next Steps
Now that you understand state management, proceed to:
- API Integration - Learn about integrating with OpenAI API
- Styling - Style your components with Tailwind CSS
- Deployment - Deploy your application
