React — Context API
Context provides a way to pass data through the component tree without having to pass props down manually at every level. It is designed for values that are considered "global" for a tree of React components: authenticated user, theme, locale, or application state.
| 1 | import { createContext, useContext, useState } from "react"; |
| 2 | |
| 3 | // Create a context with a default value |
| 4 | interface ThemeContextType { |
| 5 | theme: "light" | "dark"; |
| 6 | toggleTheme: () => void; |
| 7 | } |
| 8 | |
| 9 | const ThemeContext = createContext<ThemeContextType | undefined>(undefined); |
| 10 | |
| 11 | // Provider component — wraps children and provides context value |
| 12 | function ThemeProvider({ children }: { children: React.ReactNode }) { |
| 13 | const [theme, setTheme] = useState<"light" | "dark">("dark"); |
| 14 | |
| 15 | const toggleTheme = () => { |
| 16 | setTheme((prev) => (prev === "light" ? "dark" : "light")); |
| 17 | }; |
| 18 | |
| 19 | return ( |
| 20 | <ThemeContext.Provider value={{ theme, toggleTheme }}> |
| 21 | {children} |
| 22 | </ThemeContext.Provider> |
| 23 | ); |
| 24 | } |
| 25 | |
| 26 | // Custom hook for consuming context with error handling |
| 27 | function useTheme(): ThemeContextType { |
| 28 | const context = useContext(ThemeContext); |
| 29 | if (context === undefined) { |
| 30 | throw new Error("useTheme must be used within a ThemeProvider"); |
| 31 | } |
| 32 | return context; |
| 33 | } |
| 34 | |
| 35 | // Consuming component |
| 36 | function ThemedButton() { |
| 37 | const { theme, toggleTheme } = useTheme(); |
| 38 | |
| 39 | return ( |
| 40 | <button |
| 41 | onClick={toggleTheme} |
| 42 | className={theme === "dark" ? "bg-gray-800" : "bg-white"} |
| 43 | > |
| 44 | Current theme: {theme} |
| 45 | </button> |
| 46 | ); |
| 47 | } |
| 48 | |
| 49 | // App setup |
| 50 | function App() { |
| 51 | return ( |
| 52 | <ThemeProvider> |
| 53 | <ThemedButton /> |
| 54 | </ThemeProvider> |
| 55 | ); |
| 56 | } |
info
| 1 | // Prop drilling — passing props through intermediate components |
| 2 | // that don't use them |
| 3 | |
| 4 | // ❌ Prop drilling example |
| 5 | function App() { |
| 6 | const [user, setUser] = useState({ name: "Alice", role: "admin" }); |
| 7 | return <Layout user={user} />; // Layout doesn't use user |
| 8 | } |
| 9 | |
| 10 | function Layout({ user }: { user: User }) { |
| 11 | return ( |
| 12 | <div> |
| 13 | <Sidebar user={user} /> {/* Sidebar doesn't use user either */} |
| 14 | </div> |
| 15 | ); |
| 16 | } |
| 17 | |
| 18 | function Sidebar({ user }: { user: User }) { |
| 19 | return <UserInfo user={user} />; {/* Finally used here */} |
| 20 | } |
| 21 | |
| 22 | function UserInfo({ user }: { user: User }) { |
| 23 | return <p>{user.name} — {user.role}</p>; |
| 24 | } |
| 25 | |
| 26 | // ✅ Context eliminates prop drilling |
| 27 | const UserContext = createContext<User | null>(null); |
| 28 | |
| 29 | function App() { |
| 30 | const [user, setUser] = useState({ name: "Alice", role: "admin" }); |
| 31 | return ( |
| 32 | <UserContext.Provider value={user}> |
| 33 | <Layout /> |
| 34 | </UserContext.Provider> |
| 35 | ); |
| 36 | } |
| 37 | |
| 38 | function Layout() { |
| 39 | return <div><Sidebar /></div>; // No user prop needed |
| 40 | } |
| 41 | |
| 42 | function Sidebar() { |
| 43 | return <UserInfo />; // No user prop needed |
| 44 | } |
| 45 | |
| 46 | function UserInfo() { |
| 47 | const user = useContext(UserContext); |
| 48 | if (!user) return null; |
| 49 | return <p>{user.name} — {user.role}</p>; |
| 50 | } |
| 51 | |
| 52 | // Context is best for: |
| 53 | // - Theme (dark/light mode) |
| 54 | // - Authentication state |
| 55 | // - Locale/language |
| 56 | // - User preferences |
| 57 | // - Form state across deeply nested components |
| 58 | |
| 59 | // Context is NOT great for: |
| 60 | // - Frequently changing data (causes re-renders) |
| 61 | // - High-performance lists (use state management libraries) |
| 62 | // - Data that only one component needs (use local state) |
| 1 | // When context value changes, ALL consumers re-render |
| 2 | // This can cause performance issues with large component trees |
| 3 | |
| 4 | // ❌ Problem: entire tree re-renders on any context change |
| 5 | const AppContext = createContext({ theme: "dark", locale: "en", user: null }); |
| 6 | |
| 7 | function App() { |
| 8 | const [theme, setTheme] = useState("dark"); |
| 9 | // This changes the ENTIRE context value — all consumers re-render |
| 10 | const value = { theme, locale: "en", user: currentUser }; |
| 11 | |
| 12 | return ( |
| 13 | <AppContext.Provider value={value}> |
| 14 | <Header /> |
| 15 | <Sidebar /> |
| 16 | <Main /> |
| 17 | </AppContext.Provider> |
| 18 | ); |
| 19 | } |
| 20 | |
| 21 | // ✅ Solution 1: Split contexts by concern |
| 22 | const ThemeContext = createContext("dark"); |
| 23 | const LocaleContext = createContext("en"); |
| 24 | const UserContext = createContext<User | null>(null); |
| 25 | |
| 26 | function App() { |
| 27 | return ( |
| 28 | <ThemeContext.Provider value={theme}> |
| 29 | <LocaleContext.Provider value="en"> |
| 30 | <UserContext.Provider value={currentUser}> |
| 31 | <Layout /> |
| 32 | </UserContext.Provider> |
| 33 | </LocaleContext.Provider> |
| 34 | </ThemeContext.Provider> |
| 35 | ); |
| 36 | } |
| 37 | |
| 38 | // ✅ Solution 2: Memoize the context value |
| 39 | const MemoizedProvider = React.memo(ThemeProvider, (prev, next) => { |
| 40 | // Only re-render if theme actually changed |
| 41 | return prev.theme === next.theme; |
| 42 | }); |
| 43 | |
| 44 | // ✅ Solution 3: Use state selector pattern |
| 45 | const ThemeOnlyContext = createContext<{ |
| 46 | theme: string; |
| 47 | setTheme: (t: string) => void; |
| 48 | }>({ theme: "dark", setTheme: () => {} }); |
| 49 | |
| 50 | function useThemeOnly() { |
| 51 | return useContext(ThemeOnlyContext); |
| 52 | } |
| 53 | |
| 54 | // Now components using only theme won't re-render when locale changes |
| 55 | |
| 56 | // ❌ Creating new objects in JSX causes unnecessary re-renders |
| 57 | // <AppContext.Provider value={{ theme, locale }}> // new object every render |
| 58 | |
| 59 | // ✅ Memoize the value object |
| 60 | const contextValue = useMemo( |
| 61 | () => ({ theme, locale, user }), |
| 62 | [theme, locale, user] |
| 63 | ); |
| 64 | return <AppContext.Provider value={contextValue}>...</AppContext.Provider> |
warning
| 1 | // Combining context with useReducer for complex state logic |
| 2 | |
| 3 | interface TodoState { |
| 4 | todos: { id: string; text: string; completed: boolean }[]; |
| 5 | filter: "all" | "active" | "completed"; |
| 6 | } |
| 7 | |
| 8 | type TodoAction = |
| 9 | | { type: "ADD"; text: string } |
| 10 | | { type: "TOGGLE"; id: string } |
| 11 | | { type: "DELETE"; id: string } |
| 12 | | { type: "SET_FILTER"; filter: TodoState["filter"] }; |
| 13 | |
| 14 | function todoReducer(state: TodoState, action: TodoAction): TodoState { |
| 15 | switch (action.type) { |
| 16 | case "ADD": |
| 17 | return { |
| 18 | ...state, |
| 19 | todos: [ |
| 20 | ...state.todos, |
| 21 | { id: crypto.randomUUID(), text: action.text, completed: false }, |
| 22 | ], |
| 23 | }; |
| 24 | case "TOGGLE": |
| 25 | return { |
| 26 | ...state, |
| 27 | todos: state.todos.map((t) => |
| 28 | t.id === action.id ? { ...t, completed: !t.completed } : t |
| 29 | ), |
| 30 | }; |
| 31 | case "DELETE": |
| 32 | return { |
| 33 | ...state, |
| 34 | todos: state.todos.filter((t) => t.id !== action.id), |
| 35 | }; |
| 36 | case "SET_FILTER": |
| 37 | return { ...state, filter: action.filter }; |
| 38 | } |
| 39 | } |
| 40 | |
| 41 | const TodoContext = createContext<{ |
| 42 | state: TodoState; |
| 43 | dispatch: React.Dispatch<TodoAction>; |
| 44 | } | null>(null); |
| 45 | |
| 46 | function TodoProvider({ children }: { children: React.ReactNode }) { |
| 47 | const [state, dispatch] = useReducer(todoReducer, { |
| 48 | todos: [], |
| 49 | filter: "all", |
| 50 | }); |
| 51 | |
| 52 | return ( |
| 53 | <TodoContext.Provider value={{ state, dispatch }}> |
| 54 | {children} |
| 55 | </TodoContext.Provider> |
| 56 | ); |
| 57 | } |
| 58 | |
| 59 | function useTodo() { |
| 60 | const context = useContext(TodoContext); |
| 61 | if (!context) throw new Error("useTodo must be used within TodoProvider"); |
| 62 | return context; |
| 63 | } |
| 64 | |
| 65 | // Consuming components |
| 66 | function AddTodo() { |
| 67 | const { dispatch } = useTodo(); |
| 68 | const [text, setText] = useState(""); |
| 69 | |
| 70 | function handleSubmit(e: React.FormEvent) { |
| 71 | e.preventDefault(); |
| 72 | if (text.trim()) { |
| 73 | dispatch({ type: "ADD", text: text.trim() }); |
| 74 | setText(""); |
| 75 | } |
| 76 | } |
| 77 | |
| 78 | return ( |
| 79 | <form onSubmit={handleSubmit}> |
| 80 | <input value={text} onChange={(e) => setText(e.target.value)} /> |
| 81 | <button type="submit">Add</button> |
| 82 | </form> |
| 83 | ); |
| 84 | } |
| 85 | |
| 86 | function TodoList() { |
| 87 | const { state, dispatch } = useTodo(); |
| 88 | |
| 89 | const filtered = state.todos.filter((todo) => { |
| 90 | if (state.filter === "active") return !todo.completed; |
| 91 | if (state.filter === "completed") return todo.completed; |
| 92 | return true; |
| 93 | }); |
| 94 | |
| 95 | return ( |
| 96 | <ul> |
| 97 | {filtered.map((todo) => ( |
| 98 | <li key={todo.id}> |
| 99 | <input |
| 100 | type="checkbox" |
| 101 | checked={todo.completed} |
| 102 | onChange={() => dispatch({ type: "TOGGLE", id: todo.id })} |
| 103 | /> |
| 104 | {todo.text} |
| 105 | <button onClick={() => dispatch({ type: "DELETE", id: todo.id })}> |
| 106 | × |
| 107 | </button> |
| 108 | </li> |
| 109 | ))} |
| 110 | </ul> |
| 111 | ); |
| 112 | } |
1. Use context for truly global data: theme, authentication, locale, and user preferences.
2. Split large contexts by concern to prevent unnecessary re-renders.
3. Memoize context values with useMemo when the value object is recreated on every render.
4. Combine useReducer + context for complex state that needs multiple sub-values.
5. Don't use context for high-frequency updates — consider state management libraries like Zustand or Jotai.
6. Always wrap useContext in a custom hook with error handling.
7. Context replaces prop drilling for 3+ levels — for 1-2 levels, props are simpler.