State Management in React — Context API, Redux, and Zustand
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
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