Understanding React State
Built-in State Management
useState Hook
useState Hook
function Counter() {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
const increment = () => setCount(prev => prev + 1);
const decrement = () => setCount(prev => prev - 1);
return (
<div>
<h2>Count: {count}</h2>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder='Enter name'
/>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
); }useReducer Hook
useReducer Hook
function reducer(state, action) {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + 1 };
case 'DECREMENT':
return { ...state, count: state.count - 1 };
case 'SET_NAME':
return { ...state, name: action.payload };
case 'RESET':
return initialState;
default:
return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
const increment = () => dispatch({ type: 'INCREMENT' });
const decrement = () => dispatch({ type: 'DECREMENT' });
const setName = (name) => dispatch({ type: 'SET_NAME', payload: name });
return (
<div>
<h2>Count: {state.count}</h2>
<input
value={state.name}
onChange={(e) => setName(e.target.value)}
placeholder='Enter name'
/>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
); }Context API for Global State
Basic Context Implementation
Context API
// Create context
const AppContext = createContext(null);
// Provider component
export function AppProvider({ children }) {
const [state, dispatch] = useReducer(appReducer, initialState);
return (
<AppContext.Provider value={{ state, dispatch }}>{children}</AppContext.Provider>
); }
// Custom hook to use context
export function useApp() {
const context = useContext(AppContext);
if (!context) {
throw new Error('useApp must be used within an AppProvider');
}
return context;
}Custom Hooks for State Logic
Form State Hook
Custom Form Hook
export function useForm({ initialValues, validate, onSubmit }) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
const setValue = useCallback((name, value) => {
setValues(prev => ({ ...prev, [name]: value }));
// Clear error when user starts typing
if (errors[name]) {
setErrors(prev => ({ ...prev, [name]: undefined }));
}
}, [errors]);
const handleSubmit = useCallback(async (e) => {
e.preventDefault();
if (!validateForm()) return;
setIsSubmitting(true);
try {
await onSubmit(values);
} finally {
setIsSubmitting(false);
}
}, [values, validateForm, onSubmit]);
return {
values,
errors,
touched,
isSubmitting,
setValue,
handleSubmit
};
}External State Management Libraries
Zustand - Lightweight State Management
Zustand
import { create } from 'zustand';
interface UserStore {
user: User | null;
setUser: (user: User | null) => void;
logout: () => void;
}
export const useUserStore =
create <
UserStore >
((set) => ({
user: null,
setUser: (user) => set({ user }),
logout: () => set({ user: null }),
}));
// Usage in components
function UserProfile() {
const { user, setUser, logout } = useUserStore();
if (!user) return <div>Please log in</div>;
return (
<div>
<h2>Welcome, {user.name}!</h2>
<button onClick={logout}>Logout</button>
</div>
); }Redux Toolkit - Predictable State Management
Redux Toolkit
import { createSlice, configureStore } from '@reduxjs/toolkit';
// Slice
const usersSlice = createSlice({
name: 'users',
initialState: {
users: [],
loading: false,
error: null
},
reducers: {
addUser: (state, action) => {
state.users.push(action.payload);
},
removeUser: (state, action) => {
state.users = state.users.filter(user => user.id !== action.payload);
}
}
});
export const { addUser, removeUser } = usersSlice.actions;
// Store
export const store = configureStore({
reducer: {
users: usersSlice.reducer
}
});Server State Management
React Query (TanStack Query)
React Query
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
// Query hooks
function useUsers() {
return useQuery({
queryKey: ['users'],
queryFn: () => fetch('/api/users').then(res => res.json()),
staleTime: 5 _ 60 _ 1000, // 5 minutes
});
}
// Mutation hooks
function useCreateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (userData) =>
fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData),
}).then(res => res.json()),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
},
});
}Best Practices
State Structure
State Structure
// Good: Normalized state structure
interface AppState {
users: {
byId: Record<string, User>;
allIds: string[];
};
posts: {
byId: Record<string, Post>;
allIds: string[];
};
ui: {
loading: boolean;
error: string | null;
currentPage: number;
};
}Key Principles
- Start simple - Use useState and useReducer for local state
- Lift state up - Move shared state to common ancestors
- Use Context sparingly - Only for truly global state
- Consider external libraries - For complex state management needs
- Normalize your state - Keep data structure flat and normalized
- Optimize performance - Use memoization and selectors
- Separate concerns - Keep UI state separate from business logic