|$ curl https://forge-ai.dev/api/markdown?path=docs/react/context
$cat docs/react-—-context-api.md
updated Last week·14 min read·published

React — Context API

ReactIntermediate🎯Free Tools
Introduction

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.

createContext & useContext
create_context.tsx
TSX
1import { createContext, useContext, useState } from "react";
2
3// Create a context with a default value
4interface ThemeContextType {
5 theme: "light" | "dark";
6 toggleTheme: () => void;
7}
8
9const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
10
11// Provider component — wraps children and provides context value
12function 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
27function 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
36function 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
50function App() {
51 return (
52 <ThemeProvider>
53 <ThemedButton />
54 </ThemeProvider>
55 );
56}

info

Always create a custom hook (like useTheme) that wraps useContext. This lets you add error handling and provides a single place to change the consuming pattern.
Context vs Prop Drilling
prop_drilling.tsx
TSX
1// Prop drilling — passing props through intermediate components
2// that don't use them
3
4// ❌ Prop drilling example
5function App() {
6 const [user, setUser] = useState({ name: "Alice", role: "admin" });
7 return <Layout user={user} />; // Layout doesn't use user
8}
9
10function Layout({ user }: { user: User }) {
11 return (
12 <div>
13 <Sidebar user={user} /> {/* Sidebar doesn't use user either */}
14 </div>
15 );
16}
17
18function Sidebar({ user }: { user: User }) {
19 return <UserInfo user={user} />; {/* Finally used here */}
20}
21
22function UserInfo({ user }: { user: User }) {
23 return <p>{user.name} — {user.role}</p>;
24}
25
26// ✅ Context eliminates prop drilling
27const UserContext = createContext<User | null>(null);
28
29function 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
38function Layout() {
39 return <div><Sidebar /></div>; // No user prop needed
40}
41
42function Sidebar() {
43 return <UserInfo />; // No user prop needed
44}
45
46function 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)
Performance Implications
performance.tsx
TSX
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
5const AppContext = createContext({ theme: "dark", locale: "en", user: null });
6
7function 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
22const ThemeContext = createContext("dark");
23const LocaleContext = createContext("en");
24const UserContext = createContext<User | null>(null);
25
26function 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
39const 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
45const ThemeOnlyContext = createContext<{
46 theme: string;
47 setTheme: (t: string) => void;
48}>({ theme: "dark", setTheme: () => {} });
49
50function 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
60const contextValue = useMemo(
61 () => ({ theme, locale, user }),
62 [theme, locale, user]
63);
64return <AppContext.Provider value={contextValue}>...</AppContext.Provider>

warning

Context is not optimized for high-frequency updates. When the context value changes, every consumer component re-renders. Split frequently-changing values into separate contexts to minimize unnecessary re-renders.
Context with useReducer
context_reducer.tsx
TSX
1// Combining context with useReducer for complex state logic
2
3interface TodoState {
4 todos: { id: string; text: string; completed: boolean }[];
5 filter: "all" | "active" | "completed";
6}
7
8type TodoAction =
9 | { type: "ADD"; text: string }
10 | { type: "TOGGLE"; id: string }
11 | { type: "DELETE"; id: string }
12 | { type: "SET_FILTER"; filter: TodoState["filter"] };
13
14function 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
41const TodoContext = createContext<{
42 state: TodoState;
43 dispatch: React.Dispatch<TodoAction>;
44} | null>(null);
45
46function 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
59function 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
66function 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
86function 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}
When to Use Context

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.

$Blueprint — Engineering Documentation·Section ID: REACT-CTX·Revision: 1.0