React: Context and Global State
Last updated: 2026-10-26
1. What You'll Learn
- createContext and Provider
- useContext: consumption context
- Context vs Props Drilling
- Multi-level nesting and performance optimization
- Combining Context and useReducer (Lightweight State Management)
2. A Story About Changing Themes
(1) Pain Point: Props Passing Through 5 Levels of Components
Alice wants to add a "dark mode" toggle feature to the app. The data flow must pass through five layers of components:
▶ Example: Using Context
Output:
Context-based state shared across components
Output:
App component with state: 'light'. Renders interactive UI.
Alice's question:
themeAs the data is passed down from the App through each layer to the Button, the three intermediate layers actually don't need this data- If we add another piece of user information to pass along, the props will become extremely bloated.
- This is the classic Props Drilling
(2) Solution for "Context"
▶ Try it YourselfBenefits: Intermediate components (Page, MainLayout, Sidebar) no longer need to pass theme props; data is passed directly from the Provider to the Consumer.
3. Core Concepts of Context
| API | Function | Parameters/Return Values |
|---|---|---|
createContext(defaultValue) |
Create a context container | Back to { Provider, Consumer } |
<Context.Provider value={...}> |
Provides data to the subtree | value: Any type; triggers a Consumer re-render when it changes |
useContext(Context) |
Retrieve the most recent Provider data | Return the Provider's value; if no Provider is found, return the default value |
Custom useXxx() Hook |
Wraps useContext + error checking | Recommended pattern: Throws a clear error when Provider is missing |
(1) createContext + Provider + useContext
graph TB
A[createContext<br/>Create a context container] --> B[Provider<br/>Wrapper Component tree<br/>Provide value]
B --> C[Middle Component<br/>Don't care Context]
C --> D[useContext<br/>Consumption Context Data]
style A fill:#1890ff,color:#fff
style B fill:#52c41a,color:#fff
style D fill:#ff6b6b,color:#fff
(2) Scenarios in Which Context Is Applicable
| Suitable for Context | Not suitable for Context |
|---|---|
| Theme (Dark Mode) | Component Configuration Parameters (using Props) |
| User Login Information | Form Input Values (using local state) |
| Language/Internationalization | API Data (using TanStack Query) |
| Route State | Animation State (Managed by the animation library) |
| Global Settings | Simple Parent-to-Child Prop Passing (Using Props Directly) |
Rule: Use Context only when data needs to be shared across "multiple levels and multiple components." If you're simply passing values between parent and child components, Props are sufficient.
4. Context Nesting and Provider Componentization
(1) Multi-level Context Nesting
Real-world projects typically have multiple contexts:
function App() {
return (
<AuthProvider>
<ThemeProvider>
<I18nProvider>
<NotificationProvider>
<MainApp />
</NotificationProvider>
</I18nProvider>
</ThemeProvider>
</AuthProvider>
)
}
To avoid nesting hell, you can create a AppProviders composite component:
function AppProviders({ children }) {
return (
<AuthProvider>
<ThemeProvider>
<I18nProvider>
<NotificationProvider>
{children}
</NotificationProvider>
</I18nProvider>
</ThemeProvider>
</AuthProvider>
)
}
function App() {
return (
<AppProviders>
<MainApp />
</AppProviders>
)
}
(2) Custom Provider Hook
▶ Try it Yourself5. Context Performance Optimization
When the value of a Context changes, all components that consume that Context will re-render. This is the biggest performance pitfall of Contexts.
| Optimization Method | Principle | Applicable Scenarios |
|---|---|---|
useMemo Stable value |
Avoid creating new object references on every render | When a Provider's value contains a function or an object |
| Split by Context | "Changing" and "unchanging" data are assigned to different Contexts | Some Consumers only need stable data |
React.memo Wrapping Consumer |
Skipping re-rendering when props haven't changed | When the Consumer component receives other props |
| Selective Subscription | Use a selector to retrieve only the slices you need | Implement precise updates with Zustand/Redux |
// ❌ Question:Every time App Rendering,value They're all new partners. → All Consumer All are heavily rendered
function App() {
const [count, setCount] = useState(0)
return (
<ThemeContext.Provider value={{ theme: 'dark', setCount }}>
<ExpensiveTree />
</ThemeContext.Provider>
)
}
// ✅ Fix 1:use useMemo Stable value Quote
function App() {
const [count, setCount] = useState(0)
const themeValue = useMemo(
() => ({ theme: 'dark', toggleTheme: () => setCount(c => c + 1) }),
[]
)
return (
<ThemeContext.Provider value={themeValue}>
<ExpensiveTree />
</ThemeContext.Provider>
)
}
// ✅ Fix 2:separate "unchanged" and "changed" data into different Context
const SettingsContext = createContext({ theme: 'light', language: 'zh' })
const CountContext = createContext(0)
function App() {
const [count, setCount] = useState(0)
return (
<SettingsContext.Provider value={settingsValue}> {/* useMemo Stable */}
<CountContext.Provider value={count}>
<ExpensiveTree />
</CountContext.Provider>
</SettingsContext.Provider>
)
}
// Only when used CountContext The component will be count Re-render on changes
// SettingsContext Consumers will not be affected
6. Context + useReducer: Building Lightweight Global State
By combining Context's "cross-level propagation" with useReducer's "complex state management," you can build a lightweight global state solution:
▶ Expand runnable example (125 lines)
Output:
Displays: "Button". State: theme (setter: setTheme). Button: Button
Expected Output: A complete notification system. A notification bell (displaying the number of unread notifications) appears at the top; in the middle, users can send three types of notifications: info, success, and error; and at the bottom, a list of notifications is displayed, where each notification can be marked as read or deleted.
▶ Example 2: User Authentication Status Management (Context + Custom Hook)
▶ Expand runnable example (114 lines)
Output:
Login form with email/password. Invalid email → red border + "The email address format is incorrect." Password <6 → "Password must be at least 6 chars". Submit → console.log({email, password})
Output:
Subheading: "🔐 Log In". Displays: "🔐 Log In". State: user (setter: setUser), loading (setter: setLoading), username (setter: setUsername). Buttons: {loading ? 'Logging in......' : 'Log In'}, Log Out. Input: Username(Input admin Experience the Role of an Administrator), Password. Form with submit handling. useMemo optimizes computation. useCallback memoizes handler. Context provides global state. Async data fetching/loading states. Timer-based behavior
▶ Example 3: Combining Multiple Provider Layers with i18n Internationalization
▶ Try it YourselfOutput:
Internationalization: language switcher changes all text. Context + provider pattern for multi-language support
Output:
Subheading: "{t('greeting')}". Displays: "Chinese". State: locale (setter: setLocale). Dropdown: Chinese, English. useMemo optimizes computation. useCallback memoizes handler. Context provides global state
▶ Example 4: Splitting the Context to Optimize Performance
▶ Try it YourselfOutput:
SplitContextApp component with state: null, false. Renders interactive UI.
Output:
Displays: "Login". State: user (setter: setUser), loading (setter: setLoading). Button: login('Alice')}>Login. useMemo optimizes computation. Context provides global state. Async data fetching/loading states. Timer-based behavior
▶ Example 5: Complete Example of Using useContext as an Alternative to Props Drilling
▶ Try it Yourself
Output:
SettingsApp component with state: {
theme: 'light', fontSize: 14, sidebarCollapsed: false,
}. Renders interactive UI.
❓ FAQ
Context's value is a new object every time, causing all Consumers to re-render?useMemo to stabilize the value reference: const value = useMemo(() => ({ theme, toggleTheme }), [theme]). You can also separate "stable" and "changing" data into different Contexts. If performance issues persist after optimization, consider using Zustand (which supports precise updates and won't trigger the re-rendering of unrelated components).AppProviders mentioned earlier) and encapsulate the nesting; ② Consider whether you really need that many Contexts—some "global" data is actually just "locally global" (such as settings for a specific page) and doesn't need to be placed in the top-level Context of the App.context is null in a custom useXxx hook?Provider before using useContext, context will be null (if no default value was passed to createContext) or cause errors that are difficult to debug. Throwing a clear error message early on is much more user-friendly than having the user see “Cannot read property of null” in the browser console.📖 Summary
createContext→.Provider→useContext3 steps to use Context- Context solves Props Drilling, but does not solve the "state management" problem
- Performance Pitfall: Changes to the context value cause all consumers to be re-rendered; optimize using
useMemo - Customizing the
useXxxhook + error checking is a recommended best practice Context + useReducercan serve as a lightweight global state solution, but for complex scenarios, it’s still best to use Zustand or Redux.
📝 Exercises
- Basic Problem (Difficulty ⭐): Create a
LocaleProvider+useLocalehook that provides thelocale(zh/en) andtoggleLocale()methods to enable switching between Chinese and English. - Advanced Exercise (Difficulty ⭐⭐): Create a
AuthProvider+useAuthhook that provides theuser,login(username, password), andlogout()methods to manage the user’s authentication status. - Challenge (Difficulty: ⭐⭐⭐): Create a
CartProvider+useCarthook and use Context + useReducer to implement a shopping cart with functions to add, remove, change quantities, and clear the cart. UseuseMemoto optimize the code and avoid unnecessary re-rendering.