State Management

Context API, Redux, and Zustand for global state

When Do You Need State Management?

For simple apps, React's built-in useState and props are enough. But as your app grows, you might face "prop drilling"—passing props through many layers of components. State management solutions help share state across your app without the hassle.

Signs You Need Global State

  • • Prop drilling through 3+ levels of components
  • • Multiple components need the same data
  • • User authentication state needed everywhere
  • • Shopping cart shared across pages
  • • Theme/preferences used app-wide

Context API (Built-in)

React's built-in solution for sharing state without prop drilling:

import { createContext, useContext, useState } from 'react';

// 1. Create context
const AuthContext = createContext(null);

// 2. Create provider
function AuthProvider({ children }) {
  const [user, setUser] = useState(null);

  const login = (userData) => setUser(userData);
  const logout = () => setUser(null);

  return (
    <AuthContext.Provider value={{ user, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}

// 3. Create custom hook (recommended)
function useAuth() {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth must be used within AuthProvider');
  }
  return context;
}

// 4. Wrap app with provider
function App() {
  return (
    <AuthProvider>
      <Header />
      <MainContent />
    </AuthProvider>
  );
}

// 5. Use in any component
function Header() {
  const { user, logout } = useAuth();

  return (
    <header>
      {user ? (
        <>
          <span>Welcome, {user.name}</span>
          <button onClick={logout}>Logout</button>
        </>
      ) : (
        <Link to="/login">Login</Link>
      )}
    </header>
  );
}

Context with useReducer

import { createContext, useContext, useReducer } from 'react';

// Reducer
function cartReducer(state, action) {
  switch (action.type) {
    case 'ADD_ITEM':
      return { ...state, items: [...state.items, action.payload] };
    case 'REMOVE_ITEM':
      return { 
        ...state, 
        items: state.items.filter(item => item.id !== action.payload) 
      };
    case 'CLEAR':
      return { ...state, items: [] };
    default:
      return state;
  }
}

// Context
const CartContext = createContext(null);

function CartProvider({ children }) {
  const [state, dispatch] = useReducer(cartReducer, { items: [] });

  const addItem = (item) => dispatch({ type: 'ADD_ITEM', payload: item });
  const removeItem = (id) => dispatch({ type: 'REMOVE_ITEM', payload: id });
  const clearCart = () => dispatch({ type: 'CLEAR' });

  return (
    <CartContext.Provider value={{ 
      items: state.items, 
      addItem, 
      removeItem, 
      clearCart 
    }}>
      {children}
    </CartContext.Provider>
  );
}

function useCart() {
  return useContext(CartContext);
}

Zustand (Recommended)

Simple, fast, and scalable state management:

// npm install zustand

import { create } from 'zustand';

// Create store
const useStore = create((set) => ({
  // State
  count: 0,
  user: null,
  
  // Actions
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
  setUser: (user) => set({ user }),
  logout: () => set({ user: null }),
}));

// Use in components (no provider needed!)
function Counter() {
  const count = useStore((state) => state.count);
  const increment = useStore((state) => state.increment);

  return (
    <button onClick={increment}>Count: {count}</button>
  );
}

function UserProfile() {
  const user = useStore((state) => state.user);
  const logout = useStore((state) => state.logout);

  return user ? (
    <div>
      <p>{user.name}</p>
      <button onClick={logout}>Logout</button>
    </div>
  ) : null;
}

Zustand with TypeScript

import { create } from 'zustand';

interface User {
  id: number;
  name: string;
  email: string;
}

interface AuthState {
  user: User | null;
  isLoading: boolean;
  login: (email: string, password: string) => Promise<void>;
  logout: () => void;
}

const useAuthStore = create<AuthState>((set) => ({
  user: null,
  isLoading: false,
  
  login: async (email, password) => {
    set({ isLoading: true });
    const user = await authApi.login(email, password);
    set({ user, isLoading: false });
  },
  
  logout: () => set({ user: null }),
}));

Redux Toolkit (For Large Apps)

// npm install @reduxjs/toolkit react-redux

import { configureStore, createSlice } from '@reduxjs/toolkit';
import { Provider, useSelector, useDispatch } from 'react-redux';

// Create slice
const counterSlice = createSlice({
  name: 'counter',
  initialState: { value: 0 },
  reducers: {
    increment: (state) => { state.value += 1 },
    decrement: (state) => { state.value -= 1 },
    incrementByAmount: (state, action) => { 
      state.value += action.payload 
    },
  },
});

export const { increment, decrement, incrementByAmount } = counterSlice.actions;

// Create store
const store = configureStore({
  reducer: {
    counter: counterSlice.reducer,
  },
});

// Wrap app with Provider
function App() {
  return (
    <Provider store={store}>
      <Counter />
    </Provider>
  );
}

// Use in components
function Counter() {
  const count = useSelector((state) => state.counter.value);
  const dispatch = useDispatch();

  return (
    <div>
      <p>{count}</p>
      <button onClick={() => dispatch(increment())}>+</button>
      <button onClick={() => dispatch(decrement())}>-</button>
    </div>
  );
}

Comparison

Solution Best For Complexity
useState + Props Local state, 1-2 levels None
Context API Theme, auth, simple global state Low
Zustand Most apps, easy setup Low
Redux Toolkit Large apps, complex state Medium
Jotai/Recoil Atomic state, fine-grained Low-Medium

🎯 State Management Best Practices

  • ✓ Start with useState—add global state only when needed
  • ✓ Use Context for low-frequency updates (theme, auth)
  • ✓ Use Zustand or Redux for high-frequency updates
  • ✓ Keep state as close to where it's used as possible
  • ✓ Use React Query/TanStack Query for server state
  • ✓ Separate UI state from server/domain state
  • ✓ Avoid putting everything in global state