Skip to content

State Management in React — Context API, Redux, and Zustand

DodaTech 6 min read

In this tutorial, you'll learn about State Management in React. We cover key concepts, practical examples, and best practices.

What You'll Learn

Compare and implement three React state management approaches — Context API for built-in global state, Redux Toolkit for large-scale apps, and Zustand for lightweight stores — with practical migration paths.

Why It Matters

Choosing the wrong state management solution leads to unnecessary complexity or performance bottlenecks. Context API re-renders too many components at scale. Redux adds boilerplate. Zustand is minimal but lacks ecosystem. Each has a specific use case.

Real-World Use

A multi-step checkout form uses Context for wizard state, Redux for the product catalog and cart (shared across pages), and Zustand for UI preferences like sidebar visibility and theme — each tool doing what it does best.

Learning Path

flowchart LR
    A[React Basics] --> B[useState Hook]
    B --> C[Context API]
    C --> D[State Management]
    D --> E[Performance Optimization]
    D --> F[Final Project]
    D -->|You are here| D

Context API — Built-in Global State

The Context API ships with React and requires zero dependencies. Ideal for low-frequency updates like theme, locale, or auth status.

import { createContext, useContext, useState, ReactNode } from "react";

type Theme = "light" | "dark";

interface ThemeContextValue {
  theme: Theme;
  toggleTheme: () => void;
}

const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);

function ThemeProvider({ children }: { children: ReactNode }) {
  const [theme, setTheme] = useState<Theme>("light");

  function toggleTheme() {
    setTheme((prev) => (prev === "light" ? "dark" : "light"));
  }

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error("useTheme must be used within a ThemeProvider");
  }
  return context;
}

function ThemedButton() {
  const { theme, toggleTheme } = useTheme();
  return (
    <button
      onClick={toggleTheme}
      style={{
        background: theme === "dark" ? "#333" : "#fff",
        color: theme === "dark" ? "#fff" : "#000",
        padding: "8px 16px",
      }}
    >
      Current: {theme}
    </button>
  );
}

function App() {
  return (
    <ThemeProvider>
      <ThemedButton />
    </ThemeProvider>
  );
}

Expected output: A button that reads the current theme from context and toggles between light and dark on click. The appearance changes immediately because React re-renders all consumers when context value changes.

Redux Toolkit — Predictable State for Large Apps

TypeScript Redux Toolkit (RTK) provides a standardized way to manage global state with devtools, middleware, and the Redux DevTools browser extension.

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

interface CartItem {
  id: number;
  name: string;
  price: number;
  quantity: number;
}

interface CartState {
  items: CartItem[];
  coupon: string | null;
}

const cartSlice = createSlice({
  name: "cart",
  initialState: { items: [], coupon: null } as CartState,
  reducers: {
    addItem(state, action: PayloadAction<CartItem>) {
      const existing = state.items.find((i) => i.id === action.payload.id);
      if (existing) {
        existing.quantity += action.payload.quantity;
      } else {
        state.items.push(action.payload);
      }
    },
    removeItem(state, action: PayloadAction<number>) {
      state.items = state.items.filter((i) => i.id !== action.payload);
    },
    applyCoupon(state, action: PayloadAction<string>) {
      state.coupon = action.payload;
    },
  },
});

const { addItem, removeItem, applyCoupon } = cartSlice.actions;

const store = configureStore({
  reducer: { cart: cartSlice.reducer },
});

type RootState = ReturnType<typeof store.getState>;

function CartSummary() {
  const items = useSelector((state: RootState) => state.cart.items);
  const coupon = useSelector((state: RootState) => state.cart.coupon);
  const dispatch = useDispatch();

  const total = items.reduce((sum, i) => sum + i.price * i.quantity, 0);

  return (
    <div>
      <h2>Cart ({items.length} items)</h2>
      <ul>
        {items.map((item) => (
          <li key={item.id}>
            {item.name} x{item.quantity} — ${item.price * item.quantity}
            <button onClick={() => dispatch(removeItem(item.id))}>Remove</button>
          </li>
        ))}
      </ul>
      <p>Total: ${total.toFixed(2)}</p>
      {coupon && <p>Coupon applied: {coupon}</p>}
      <button onClick={() => dispatch(applyCoupon("SAVE10"))}>
        Apply Coupon
      </button>
    </div>
  );
}

function App() {
  return (
    <Provider store={store}>
      <CartSummary />
    </Provider>
  );
}

Expected output: Initially shows "Cart (0 items)" with total $0.00. Items can be added via dispatch, removed with the Remove button, and a coupon applied with the Apply Coupon button. The Redux DevTools shows every action and state change.

Zustand — Lightweight Store

Zustand is a minimal state management library with a hook-based API. No providers, no reducers, no boilerplate.

import { create } from "zustand";
import { persist } from "zustand/middleware";

interface UIState {
  sidebarOpen: boolean;
  fontSize: number;
  toggleSidebar: () => void;
  setFontSize: (size: number) => void;
}

const useUIStore = create<UIState>()(
  persist(
    (set) => ({
      sidebarOpen: true,
      fontSize: 16,
      toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })),
      setFontSize: (size) => set({ fontSize: size }),
    }),
    { name: "ui-preferences" }
  )
);

function Sidebar() {
  const sidebarOpen = useUIStore((state) => state.sidebarOpen);
  const toggleSidebar = useUIStore((state) => state.toggleSidebar);

  return (
    <div style={{ display: "flex" }}>
      <aside
        style={{
          width: sidebarOpen ? 240 : 0,
          overflow: "hidden",
          transition: "width 0.3s",
          background: "#f5f5f5",
        }}
      >
        <nav>Menu items here...</nav>
      </aside>
      <button onClick={toggleSidebar}>
        {sidebarOpen ? "Close" : "Open"} Sidebar
      </button>
    </div>
  );
}

function FontSizeControl() {
  const fontSize = useUIStore((state) => state.fontSize);
  const setFontSize = useUIStore((state) => state.setFontSize);

  return (
    <div>
      <label>
        Font size: {fontSize}px
        <input
          type="range"
          min="12"
          max="24"
          value={fontSize}
          onChange={(e) => setFontSize(Number(e.target.value))}
        />
      </label>
    </div>
  );
}

Expected output: Sidebar toggles open/closed with animation. Font size slider persists the preference across page refreshes (stored in localStorage via the persist middleware). Notice no Provider wrapper is needed — Zustand stores are consumed directly.

Comparison Table

Feature Context API Redux Toolkit Zustand
Bundle size 0 KB ~11 KB ~1 KB
Boilerplate Low Medium Minimal
DevTools None Excellent Plugin
Middleware None Built-in Plugin
Best for Low-frequency state Complex apps Simple stores

Common Mistakes

1. Using Context for High-Frequency Updates

Every context value change re-renders ALL consumers. For real-time data like mouse position or frames, use Zustand or Redux.

2. Putting Everything in Redux

Not all state belongs in a global store. Form input values, toggle states, and component-specific data should stay in local state or Context.

3. Mutating State in Redux Reducers

Redux Toolkit uses Immer internally, which allows "mutative" syntax safely. But with vanilla Redux, never mutate state directly.

Practice Questions

When should I use Context API instead of Redux?

Use Context for low-frequency, low-complexity global state like theme, locale, or auth status where the value changes infrequently. Use Redux when you need middleware, devtools, or when multiple unrelated components share complex state with frequent updates.

Does Zustand replace Redux?

Not entirely. Zustand is excellent for small to medium apps and UI state. Redux remains better for large applications requiring middleware chains, normalized data, time-travel debugging, or a strict data flow pattern enforced by the team.

How do I prevent unnecessary re-renders with Context?

Split context by domain (AuthContext, ThemeContext, UIContext) so changing one does not re-render consumers of another. For high-frequency updates, use Zustand or Redux which allow granular subscriptions.

Challenge

Build a small e-commerce app that combines all three approaches: Context for auth state, Redux for the product catalog and cart, and Zustand for UI preferences. Each tool manages its domain without overlapping.

Real-World Task

Migrate a component that passes props through five levels of nesting to Context API. Then identify which pieces of state would benefit from Redux (shared across unrelated sections) and which are UI-local (belong in Zustand). Document your migration decisions.


This tutorial is built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro — production applications that use Context for auth, Redux for complex workflows, and Zustand for UI preferences at scale.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro