Here's what confuses every React developer:
"When do I need Redux? Zustand? Context? What about hooks?"
Every article says something different. Every example uses different approach.
Let me clear this up once and for all.
This guide shows you exactly when to use each state management option—with code examples.
State Management Spectrum: Simple to Complex
Not all state is same. Different problems need different solutions.
Level 1: Local Component State (Simplest)
Use For:
- Form inputs (name, email, password)
- UI state (open/close modals, tabs, dropdowns)
- Component-level toggles (dark mode, sidebar)
- Temporary data (loading states, error messages)
Examples:
javascriptconst [name, setName] = useState("");const [isOpen, setIsOpen] = useState(false);const [isLoading, setIsLoading] = useState(false);
Don't Use External Libraries For:
- Simple form fields
- Component toggles
- UI-only state
Level 2: React Context (Mid Complexity)
Use For:
- Theme (dark mode, language)
- User authentication status
- Settings that multiple components need
- Data accessed by many components (not updated frequently)
When Context Works Well:
- State doesn't change often
- Children read more than write
- Simple data structures (user, theme, settings)
- Performance isn't critical concern
Example:
javascript// Create contextconst AuthContext = createContext();// Providerexport function AuthProvider({ children }) {const [user, setUser] = useState(null);const login = async (email, password) => {const user = await api.login(email, password);setUser(user);};return (<AuthContext.Provider value={{ user, login, logout }}>{children}</AuthContext.Provider>);}// Consumeexport function useAuth() {return useContext(AuthContext);}
Level 3: State Management Library (Complex)
Use For:
- Complex state with many actions
- State accessed by many components
- Frequent updates across components
- Performance matters for large state
- Need time-travel debugging (undo/redo)
Popular Libraries (2025):
- Zustand: Modern, simple, fast
- Jotai: Atomic, flexible, powerful
- Redux Toolkit: Mature, ecosystem, Redux DevTools
- Recoil: Facebook's solution, experimental but good
When to Use Each Approach
Decision Framework
Situation | Use This Approach |
|---|---|
Form inputs, UI toggles | Local |
Theme, auth, simple settings | Context API |
Multiple components write frequently | Zustand/Jotai |
Complex state with many actions | Redux Toolkit |
Need undo/redo/time-travel | Redux Toolkit |
Performance critical with many updates | Zustand or Jotai |
Example Scenarios
Scenario 1: Shopping Cart (Complex)
javascript// Good: Zustandconst useCartStore = create((set) => ({items: [],addItem: (item) => set((state) => ({ items: [...state.items, item] })),removeItem: (id) =>set((state) => ({ items: state.items.filter((i) => i.id !== id) })),clear: () => set({ items: [] }),}));
Scenario 2: Theme (Simple)
javascript// Good: Contextconst ThemeContext = createContext();export function ThemeProvider({ children }) {const [theme, setTheme] = useState("light");return (<ThemeContext.Provider value={{ theme, setTheme }}>{children}</ThemeContext.Provider>);}
React Hooks: Use Before External Libraries
Built-in hooks are powerful. Use them first.
useState
For: Simple, independent state in component
javascriptconst [count, setCount] = useState(0);
useReducer
For: Complex state logic with multiple related state values
javascriptconst initialState = { count: 0, name: "" };function reducer(state, action) {switch (action.type) {case "increment":return { ...state, count: state.count + 1 };case "setName":return { ...state, name: action.payload };default:return state;}}const [state, dispatch] = useReducer(reducer, initialState);
useEffect
For: Side effects (API calls, subscriptions, DOM manipulation)
javascriptuseEffect(() => {// Runs after renderfetchData();}, []); // Empty deps = run onceuseEffect(() => {// Runs when userId changesfetchUserData(userId);}, [userId]);
useCallback
For: Memoizing functions to prevent unnecessary re-renders
javascriptconst handleClick = useCallback(() => {doSomething(id);}, [id]); // Only recreates when id changes
useMemo
For: Expensive calculations
javascriptconst expensiveValue = useMemo(() => {return computeExpensive(data);}, [data]); // Only re-computes when data changes
React Context: For Shared State
Context is built-in, works well for right use cases.
When to Use Context
✅ Good Use Cases:
- User authentication (login status)
- Theme (dark/light mode)
- Language (i18n)
- App-wide settings
- Data read often, written rarely
❌ Bad Use Cases:
- Frequently updated data (real-time feeds, chat)
- Complex state with many actions
- Performance-critical updates
- State that changes on every render
Context Best Practices
1. Split Multiple Contexts
javascript// Bad: Everything in one contextconst AppContext = createContext({user: null,theme: "light",cart: [],notifications: [],settings: {},});// Good: Separate concernsconst UserContext = createContext({ user, login, logout });const ThemeContext = createContext({ theme, setTheme });const CartContext = createContext({ cart, addItem, removeItem });
2. Memoize Context Providers
javascript// Prevents re-renders when parent updatesexport function AppProviders({ children }) {return (<AuthProvider><ThemeProvider><CartProvider>{children}</CartProvider></ThemeProvider></AuthProvider>);}
3. Optimize Context Values
javascript// Bad: Re-creates object every render<UserContext.Provider value={{ user, login, logout }}>// Good: Memoize if expensive<UserContext.Provider value={{ user, login, logout }}>
Zustand: Modern State Management (Recommended)
Zustand is our favorite for most use cases in 2025.
Why Zustand?
Pros:
- No boilerplate (vs Redux)
- No providers needed (Context-based under hood)
- TypeScript support excellent
- Fast (no context re-renders)
- Simple API
- Tiny bundle (1KB)
Cons:
- Smaller ecosystem than Redux
- No DevTools (yet, though coming)
- Newer (less battle-tested than Redux)
Zustand Example
typescript// store.tsimport create from "zustand";interface User {id: string;name: string;email: string;}interface AuthStore {user: User | null;isLoading: boolean;login: (email: string, password: string) => Promise<void>;logout: () => void;}const useAuthStore = create<AuthStore>((set) => ({user: null,isLoading: false,login: async (email, password) => {set({ isLoading: true });try {const user = await api.login(email, password);set({ user, isLoading: false });} catch (error) {set({ isLoading: false });throw error;}},logout: () => {set({ user: null });api.logout();},}));// Component usagefunction Login() {const { login, isLoading } = useAuthStore();const handleSubmit = async (e) => {e.preventDefault();await login(email, password);};return <form onSubmit={handleSubmit}>...</form>;}
Jotai: Atomic State Management
Jotai is great for very complex, deeply nested state.
Why Jotai?
Pros:
- Atomic state (small, independent pieces)
- Flexible composition
- TypeScript excellent
- Very small bundle
- No providers needed
Cons:
- More verbose than Zustand for simple cases
- Newer, smaller ecosystem
Jotai Example
typescript// store.tsimport { atom, useAtom } from "jotai";// Atomic state piecesexport const countAtom = atom(0);export const nameAtom = atom("");export const emailAtom = atom("");// Component usagefunction Counter() {const [count, setCount] = useAtom(countAtom);return (<div><button onClick={() => setCount(count + 1)}>{count}</button></div>);}
Redux Toolkit: For Complex Apps
Redux is mature but has boilerplate. Redux Toolkit fixes this.
When to Use Redux Toolkit
- Very complex state (dozens of actions, reducers)
- Need time-travel debugging
- Team already knows Redux
- Need middleware (thunk, saga, etc.)
- Want large ecosystem support
Redux Toolkit Example
typescript// store/slice.tsimport { createSlice, PayloadAction } from "@reduxjs/toolkit";interface CounterState {value: number;}const initialState: CounterState = {value: 0,};export const counterSlice = createSlice({name: "counter",initialState,reducers: {increment: (state) => {state.value += 1;},decrement: (state) => {state.value -= 1;},incrementByAmount: (state, action: PayloadAction<number>) => {state.value += action.payload;},},});export const { increment, decrement, incrementByAmount } = counterSlice.actions;
State Management Decision Tree
Use this flowchart to decide:
┌─────────────────────────────┐
│ Is state local to component? │
└──────────────┬──────────────┘
│ Yes
↓
Use useState/useReducer
│ No
↓
┌──────────────────────────────┐
│ Is state complex or updates │
│ frequently across components? │
└──────────────┬───────────────┘
│ Yes
↓
┌─────────────────────────────┐
│ Do you need DevTools or │
│ time-travel debugging? │
└──────────────┬──────────────┘
│ Yes │ No
↓ ↓
Redux Toolkit Zustand/Jotai
Performance: When State Management Hurts
Common Performance Issues
1. Unnecessary Re-renders
javascript// Bad: Re-renders every render<Parent><Child onClick={handleClick} /></Parent>// Good: Memoize function<Parent><Child onClick={useCallback(handleClick, [dependency])} /></Parent>
2. Large State Objects
javascript// Bad: Entire user object in Context<UserContext.Provider value={{ user }}>// Good: Only what's needed<UserContext.Provider value={{ userId, userName }}>
3. Frequent Context Updates
javascript// Bad: Context updates every second<TimerContext.Provider value={{ currentTime }}> // All consumers re-render// Good: Use ref or state library with selective subscriptions
State Management Best Practices
1. Keep State as Simple as Possible
Start with useState. Only escalate when you clearly need more.
2. Colocate Related State
Don't scatter related state across different stores.
Bad:
javascriptconst useUserStore = ...;const useUserProfileStore = ...;const useUserSettingsStore = ...;const useUserNotificationsStore = ...;
Good:
javascriptconst useUserStore = ...; // Everything user-related together
3. Derive State When Possible
Don't duplicate data, compute it.
javascript// Bad: Store both first and last nameconst userStore = create((set) => ({firstName: "",lastName: "",fullName: "", // Duplicated!}));// Good: Compute fullNamefunction Component() {const { firstName, lastName } = useUserStore();const fullName = `${firstName} ${lastName}`; // Derived, not stored}
4. Use TypeScript
State is your most critical data. Type it.
typescriptinterface User {id: string;name: string;email: string;}interface UserStore {user: User | null;login: (email: string, password: string) => Promise<void>;}
State Management Comparison 2025
Feature | useState | Context | Zustand | Jotai | Redux Toolkit |
|---|---|---|---|---|---|
Bundle Size | 0KB | ~1KB | ~1KB | ~3KB | ~5KB |
TypeScript | Excellent | Good | Excellent | Excellent | Excellent |
Performance | Fast | Medium (re-renders) | Fast | Fast | Fast |
DevTools | Browser DevTools | Browser DevTools | Planning | Browser DevTools | Redux DevTools |
Ecosystem | React | React | Growing | Growing | Largest |
Learning Curve | Easy | Easy | Easy | Easy | Medium |
Use Cases | Local | Simple shared | Most cases | Atomic | Complex |
Bundle Size | 0KB | ~1KB | ~1KB | ~3KB | ~5KB |
|---|---|---|---|---|---|
TypeScript | Excellent | Good | Excellent | Excellent | Excellent |
Performance | Fast | Medium (re-renders) | Fast | Fast | Fast |
DevTools | Browser DevTools | Browser DevTools | Planning | Browser DevTools | Redux DevTools |
Ecosystem | React | React | Growing | Growing | Largest |
Learning Curve | Easy | Easy | Easy | Easy | Medium |
Use Cases | Local | Simple shared | Most cases | Atomic | Complex |
Related Reading
If you found this helpful, you might also enjoy:
- React Component Architecture Patterns - Component structure
- Frontend Testing Guide - Test your state
- Next.js App Router Best Practices 2025 - Framework-specific
- Building Accessible React Applications - Inclusive state
Need Help Choosing State Management?
At Startupbricks, we've built dozens of React apps with different state management approaches. We know what works for different use cases, team sizes, and performance requirements.
Whether you need:
- State management architecture design
- Performance optimization
- Code review and refactoring
- Team training and best practices
Let's talk about choosing right state management for your app.
Ready to choose? Download our free State Management Decision Tree and start today.
