REACT & NEXT.JS / 4. CONTEXT & STATE MANAGEMENT

Context API & State Management

Sharing state without prop drilling


EXPLANATION

Prop drilling: passing props through many layers of components just to reach a deeply nested child. Context solves this.

Context: a way to share values between components without passing props at every level.

Three parts:
1. createContext() → creates the context object
2. <Context.Provider value={...}> → wraps components that need access
3. useContext(Context) → reads the value inside any descendant

When to use Context:
• Theme (dark/light mode)
• Authentication state (current user)
• Language/locale
• Global UI state (sidebar open/closed)

When NOT to use Context:
• Frequently changing state (causes all consumers to re-render)
• Server state (use React Query / SWR instead)
• Complex state logic (use Zustand or Redux)

Custom hook pattern: wrap useContext in a custom hook for better DX and error handling.

Zustand: lightweight state management library. Much simpler than Redux, better than Context for frequently updated state.

DIAGRAM

Without Context (prop drilling):
  App(user) → Layout(user) → Sidebar(user) → Avatar(user)
  user passed through every layer even if Layout/Sidebar don't need it

  With Context:
  <UserContext.Provider value={user}>
    <Layout>          ← doesn't receive user
      <Sidebar>       ← doesn't receive user
        <Avatar />    ← useContext(UserContext) → gets user directly
      </Sidebar>
    </Layout>
  </UserContext.Provider>

  Zustand store:
  create(set => ({
    count: 0,
    increment: () => set(state => ({ count: state.count + 1 }))
  }))
  → use in any component, no Provider needed

CODE

TSX
1"use client";
2
3import { createContext, useContext, useState, useCallback } from "react";
4
5// ── Theme context ─────────────────────────────────────────────────
6type Theme = "dark" | "light";
7
8type ThemeContextType = {
9 theme: Theme;
10 toggleTheme: () => void;
11};
12
13const ThemeContext = createContext<ThemeContextType | null>(null);
14
15// Custom hook better than useContext directly
16export function useTheme() {
17 const ctx = useContext(ThemeContext);
18 if (!ctx) throw new Error("useTheme must be used within ThemeProvider");
19 return ctx;
20}
21
22export function ThemeProvider({ children }: { children: React.ReactNode }) {
23 const [theme, setTheme] = useState<Theme>("dark");
24 const toggleTheme = useCallback(() => {
25 setTheme(prev => prev === "dark" ? "light" : "dark");
26 }, []);
27
28 return (
29 <ThemeContext.Provider value={{ theme, toggleTheme }}>
30 <div data-theme={theme}>{children}</div>
31 </ThemeContext.Provider>
32 );
33}
34
35// Use anywhere in the tree
36function ThemeToggleButton() {
37 const { theme, toggleTheme } = useTheme();
38 return <button onClick={toggleTheme}>{theme === "dark" ? "☀️" : "🌙"}</button>;
39}
40
41// ── Auth context ─────────────────────────────────────────────────
42type User = { id: string; name: string; email: string };
43
44type AuthContextType = {
45 user: User | null;
46 login: (email: string, password: string) => Promise<void>;
47 logout: () => void;
48};
49
50const AuthContext = createContext<AuthContextType | null>(null);
51
52export function useAuth() {
53 const ctx = useContext(AuthContext);
54 if (!ctx) throw new Error("useAuth must be used within AuthProvider");
55 return ctx;
56}
57
58export function AuthProvider({ children }: { children: React.ReactNode }) {
59 const [user, setUser] = useState<User | null>(null);
60
61 const login = useCallback(async (email: string, password: string) => {
62 const res = await fetch("/api/auth/login", {
63 method: "POST",
64 body: JSON.stringify({ email, password }),
65 });
66 const data = await res.json();
67 setUser(data.user);
68 }, []);
69
70 const logout = useCallback(() => {
71 setUser(null);
72 fetch("/api/auth/logout", { method: "POST" });
73 }, []);
74
75 return (
76 <AuthContext.Provider value={{ user, login, logout }}>
77 {children}
78 </AuthContext.Provider>
79 );
80}
81
82// ── Zustand (install: bun add zustand) ────────────────────────────
83// Much simpler than Redux, no Provider needed
84import { create } from "zustand";
85
86type StoreState = {
87 count: number;
88 items: string[];
89 increment: () => void;
90 addItem: (item: string) => void;
91 reset: () => void;
92};
93
94export const useStore = create<StoreState>(set => ({
95 count: 0,
96 items: [],
97 increment: () => set(state => ({ count: state.count + 1 })),
98 addItem: (item) => set(state => ({ items: [...state.items, item] })),
99 reset: () => set({ count: 0, items: [] }),
100}));
101
102// Use in any component no Provider wrapping needed
103function Counter() {
104 const { count, increment } = useStore();
105 return <button onClick={increment}>Count: {count}</button>;
106}
← PREV3. useRef, useMemo, useCallbackNEXT →5. App Router & Layouts