React offers many state management options. This guide compares modern approaches to help you choose the right solution for your needs.
Local State: useState and useReducer#
useState for Simple State#
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>;
}useReducer for Complex State#
1type Action = { type: 'increment' } | { type: 'decrement' } | { type: 'reset' };
2
3function reducer(state: number, action: Action): number {
4 switch (action.type) {
5 case 'increment': return state + 1;
6 case 'decrement': return state - 1;
7 case 'reset': return 0;
8 }
9}
10
11function Counter() {
12 const [count, dispatch] = useReducer(reducer, 0);
13 return <button onClick={() => dispatch({ type: 'increment' })}>{count}</button>;
14}Zustand#
Minimal, flexible store:
1import { create } from 'zustand';
2
3const useStore = create((set) => ({
4 count: 0,
5 increment: () => set((state) => ({ count: state.count + 1 })),
6}));
7
8function Counter() {
9 const { count, increment } = useStore();
10 return <button onClick={increment}>{count}</button>;
11}Jotai#
Atomic state management:
1import { atom, useAtom } from 'jotai';
2
3const countAtom = atom(0);
4
5function Counter() {
6 const [count, setCount] = useAtom(countAtom);
7 return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
8}React Query for Server State#
1import { useQuery, useMutation } from '@tanstack/react-query';
2
3function UserList() {
4 const { data, isLoading } = useQuery({
5 queryKey: ['users'],
6 queryFn: () => fetch('/api/users').then(r => r.json()),
7 });
8
9 if (isLoading) return <div>Loading...</div>;
10 return <ul>{data.map(user => <li key={user.id}>{user.name}</li>)}</ul>;
11}When to Use What#
- useState: Local component state
- Context: Theme, auth, infrequent updates
- Zustand/Jotai: Global client state
- React Query: Server state and caching
- Redux: Complex apps with middleware needs
Start simple and add complexity only when needed.