Back to Blog
ReactState ManagementReduxZustand

React State Management: A Comparison

Choose the right state management. From useState to Context to Zustand to Redux patterns.

B
Bootspring Team
Engineering
August 15, 2022
6 min read

Choosing the right state management approach depends on your needs. Here's how different solutions compare.

When to Use What#

Local state (useState): - Component-specific data - Form inputs - UI state (open/closed) Lifted state: - Shared between few components - Parent-child relationships - Simple prop drilling acceptable Context: - Theme, locale, auth - Infrequently changing data - Avoiding deep prop drilling External stores (Zustand, Redux): - Complex state logic - Frequent updates - Shared across many components - DevTools debugging needed

useState and useReducer#

1// Simple state 2function Counter() { 3 const [count, setCount] = useState(0); 4 5 return ( 6 <button onClick={() => setCount(c => c + 1)}> 7 Count: {count} 8 </button> 9 ); 10} 11 12// Complex state with useReducer 13interface State { 14 items: Item[]; 15 loading: boolean; 16 error: string | null; 17} 18 19type Action = 20 | { type: 'FETCH_START' } 21 | { type: 'FETCH_SUCCESS'; payload: Item[] } 22 | { type: 'FETCH_ERROR'; payload: string } 23 | { type: 'ADD_ITEM'; payload: Item }; 24 25function reducer(state: State, action: Action): State { 26 switch (action.type) { 27 case 'FETCH_START': 28 return { ...state, loading: true, error: null }; 29 case 'FETCH_SUCCESS': 30 return { ...state, loading: false, items: action.payload }; 31 case 'FETCH_ERROR': 32 return { ...state, loading: false, error: action.payload }; 33 case 'ADD_ITEM': 34 return { ...state, items: [...state.items, action.payload] }; 35 default: 36 return state; 37 } 38} 39 40function ItemList() { 41 const [state, dispatch] = useReducer(reducer, { 42 items: [], 43 loading: false, 44 error: null, 45 }); 46 47 // Use dispatch to update state 48}

React Context#

1// Create context 2interface AuthContextType { 3 user: User | null; 4 login: (credentials: Credentials) => Promise<void>; 5 logout: () => void; 6} 7 8const AuthContext = createContext<AuthContextType | null>(null); 9 10// Provider 11function AuthProvider({ children }: { children: ReactNode }) { 12 const [user, setUser] = useState<User | null>(null); 13 14 const login = async (credentials: Credentials) => { 15 const user = await authApi.login(credentials); 16 setUser(user); 17 }; 18 19 const logout = () => { 20 authApi.logout(); 21 setUser(null); 22 }; 23 24 return ( 25 <AuthContext.Provider value={{ user, login, logout }}> 26 {children} 27 </AuthContext.Provider> 28 ); 29} 30 31// Hook 32function useAuth() { 33 const context = useContext(AuthContext); 34 if (!context) { 35 throw new Error('useAuth must be used within AuthProvider'); 36 } 37 return context; 38} 39 40// Usage 41function Profile() { 42 const { user, logout } = useAuth(); 43 44 if (!user) return <LoginPrompt />; 45 46 return ( 47 <div> 48 <p>Welcome, {user.name}</p> 49 <button onClick={logout}>Logout</button> 50 </div> 51 ); 52}

Zustand#

1import { create } from 'zustand'; 2import { devtools, persist } from 'zustand/middleware'; 3 4interface CartState { 5 items: CartItem[]; 6 addItem: (item: CartItem) => void; 7 removeItem: (id: string) => void; 8 clearCart: () => void; 9 total: () => number; 10} 11 12const useCartStore = create<CartState>()( 13 devtools( 14 persist( 15 (set, get) => ({ 16 items: [], 17 18 addItem: (item) => 19 set((state) => ({ 20 items: [...state.items, item], 21 })), 22 23 removeItem: (id) => 24 set((state) => ({ 25 items: state.items.filter((item) => item.id !== id), 26 })), 27 28 clearCart: () => set({ items: [] }), 29 30 total: () => 31 get().items.reduce((sum, item) => sum + item.price * item.quantity, 0), 32 }), 33 { name: 'cart-storage' } 34 ) 35 ) 36); 37 38// Usage - no provider needed 39function Cart() { 40 const { items, removeItem, total } = useCartStore(); 41 42 return ( 43 <div> 44 {items.map((item) => ( 45 <div key={item.id}> 46 {item.name} - ${item.price} 47 <button onClick={() => removeItem(item.id)}>Remove</button> 48 </div> 49 ))} 50 <p>Total: ${total()}</p> 51 </div> 52 ); 53} 54 55// Select specific slice (prevents unnecessary re-renders) 56function CartCount() { 57 const count = useCartStore((state) => state.items.length); 58 return <span>Items: {count}</span>; 59}

Redux Toolkit#

1import { createSlice, configureStore, PayloadAction } from '@reduxjs/toolkit'; 2import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; 3 4// Slice 5interface TodoState { 6 items: Todo[]; 7 filter: 'all' | 'active' | 'completed'; 8} 9 10const todoSlice = createSlice({ 11 name: 'todos', 12 initialState: { items: [], filter: 'all' } as TodoState, 13 reducers: { 14 addTodo: (state, action: PayloadAction<string>) => { 15 state.items.push({ 16 id: crypto.randomUUID(), 17 text: action.payload, 18 completed: false, 19 }); 20 }, 21 toggleTodo: (state, action: PayloadAction<string>) => { 22 const todo = state.items.find((t) => t.id === action.payload); 23 if (todo) { 24 todo.completed = !todo.completed; 25 } 26 }, 27 setFilter: (state, action: PayloadAction<TodoState['filter']>) => { 28 state.filter = action.payload; 29 }, 30 }, 31}); 32 33// Store 34const store = configureStore({ 35 reducer: { 36 todos: todoSlice.reducer, 37 }, 38}); 39 40type RootState = ReturnType<typeof store.getState>; 41type AppDispatch = typeof store.dispatch; 42 43// Typed hooks 44const useAppDispatch: () => AppDispatch = useDispatch; 45const useAppSelector: TypedUseSelectorHook<RootState> = useSelector; 46 47// Usage 48function TodoList() { 49 const dispatch = useAppDispatch(); 50 const todos = useAppSelector((state) => state.todos.items); 51 const filter = useAppSelector((state) => state.todos.filter); 52 53 const filteredTodos = todos.filter((todo) => { 54 if (filter === 'active') return !todo.completed; 55 if (filter === 'completed') return todo.completed; 56 return true; 57 }); 58 59 return ( 60 <ul> 61 {filteredTodos.map((todo) => ( 62 <li 63 key={todo.id} 64 onClick={() => dispatch(todoSlice.actions.toggleTodo(todo.id))} 65 > 66 {todo.text} 67 </li> 68 ))} 69 </ul> 70 ); 71}

Jotai (Atomic State)#

1import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai'; 2 3// Primitive atoms 4const countAtom = atom(0); 5const textAtom = atom(''); 6 7// Derived atom 8const doubledCountAtom = atom((get) => get(countAtom) * 2); 9 10// Writable derived atom 11const countWithValidationAtom = atom( 12 (get) => get(countAtom), 13 (get, set, newValue: number) => { 14 if (newValue >= 0) { 15 set(countAtom, newValue); 16 } 17 } 18); 19 20// Async atom 21const userAtom = atom(async () => { 22 const response = await fetch('/api/user'); 23 return response.json(); 24}); 25 26// Usage 27function Counter() { 28 const [count, setCount] = useAtom(countAtom); 29 const doubled = useAtomValue(doubledCountAtom); 30 31 return ( 32 <div> 33 <p>Count: {count}</p> 34 <p>Doubled: {doubled}</p> 35 <button onClick={() => setCount((c) => c + 1)}>Increment</button> 36 </div> 37 ); 38}

Comparison Table#

| Feature | useState | Context | Zustand | Redux | Jotai | |------------------|----------|---------|---------|--------|--------| | Boilerplate | Low | Medium | Low | Medium | Low | | Learning curve | Easy | Easy | Easy | Medium | Easy | | DevTools | React | React | Yes | Yes | Yes | | Persistence | Manual | Manual | Built-in| Manual | Manual | | Bundle size | 0 | 0 | ~3KB | ~11KB | ~4KB | | Re-render control| Manual | Poor | Good | Good | Good | | Async support | Manual | Manual | Manual | RTK Q | Built-in| | SSR support | Yes | Yes | Yes | Yes | Yes |

Best Practices#

General: ✓ Start simple, add complexity as needed ✓ Colocate state with usage ✓ Avoid premature optimization ✓ Use selectors for derived data Context: ✓ Split contexts by domain ✓ Memoize provider values ✓ Avoid frequent updates External stores: ✓ Normalize complex data ✓ Use selectors to prevent re-renders ✓ Keep actions small and focused ✓ Leverage middleware for side effects

Conclusion#

Start with useState for local state, use Context for app-wide infrequent data like themes. For complex or frequently updating state, Zustand offers simplicity while Redux provides structure. Choose based on team experience and app complexity.

Share this article

Help spread the word about Bootspring