React: Context and Global State
Last updated: 2026-08-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
// Requirements:User switches to dark mode,All child components change accordingly
// Layer 5: innermost Button needs to Know theme
function Button({ theme }) {
return <button style={{
backgroundColor: theme === 'dark' ? '#333' : '#f0f0f0',
color: theme === 'dark' ? 'white' : '#333'
}}>Button</button>
}
// Layer 4
function Sidebar({ theme }) {
return <div><Button theme={theme} /></div>
}
// Layer 3
function MainLayout({ theme }) {
return <div><Sidebar theme={theme} /></div>
}
// Layer 2
function Page({ theme }) {
return <MainLayout theme={theme} />
}
// Layer 1: App has theme state
function App() {
const [theme, setTheme] = useState('light')
return <Page theme={theme} /> // ❌ theme penetrates 5 layers
}
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"
// 1. Create Context
const ThemeContext = React.createContext('light')
// 2. Provider Provide data
function App() {
const [theme, setTheme] = useState('light')
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Page />
</ThemeContext.Provider>
)
}
// The middle component is completely unconcerned with theme
function Page() { return <MainLayout /> }
function MainLayout() { return <Sidebar /> }
function Sidebar() { return <Button /> }
// 5. Only the necessary components are loaded Context
function Button() {
const { theme, setTheme } = useContext(ThemeContext)
return (
<button style={{
backgroundColor: theme === 'dark' ? '#333' : '#f0f0f0',
color: theme === 'dark' ? 'white' : '#333'
}}
onClick={() => setTheme(t => t === 'dark' ? 'light' : 'dark')}>
{theme === 'dark' ? '🌙' : '☀️'} Switch Themes
</button>
)
}
Benefits: 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
// 1. Create Context(You can specify a default value)
const UserContext = createContext({ name: 'Guest', role: 'guest' })
const ThemeContext = createContext('light')
// 2. Provider Provide data(The component tree that the wrapper needs to access)
function App() {
const [user, setUser] = useState({ name: 'Alice', role: 'admin' })
return (
<UserContext.Provider value={{ user, setUser }}>
<ThemeContext.Provider value="dark">
<Dashboard />
</ThemeContext.Provider>
</UserContext.Provider>
)
}
// 3. Any child component reads Context
function Dashboard() {
const { user } = useContext(UserContext)
const theme = useContext(ThemeContext)
return <p>{user.name} - Topic:{theme}</p>
}
(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
// ============================================
// Example:Custom ThemeProvider + useTheme Hook
// ============================================
const ThemeContext = React.createContext(null)
function ThemeProvider({ children }) {
const [theme, setTheme] = React.useState(() => {
// from localStorage Read the initial topic
return localStorage.getItem('theme') || 'light'
})
function toggleTheme() {
setTheme(prev => {
const next = prev === 'light' ? 'dark' : 'light'
localStorage.setItem('theme', next) // Persistence
return next
})
}
const value = React.useMemo(
() => ({ theme, toggleTheme }),
[theme]
)
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
)
}
// Custom Hook:Packaging useContext + Error Checking
function useTheme() {
const context = useContext(ThemeContext)
if (!context) {
throw new Error('useTheme Must be ThemeProvider For internal use only')
}
return context
}
// Usage
function ThemedButton() {
const { theme, toggleTheme } = useTheme()
return (
<button onClick={toggleTheme}
style={{
backgroundColor: theme === 'dark' ? '#333' : '#f0f0f0',
color: theme === 'dark' ? 'white' : '#333',
padding: '10px 20px',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}>
Current Topic:{theme === 'dark' ? '🌙' : '☀️'} Click to toggle
</button>
)
}
5. 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:
// ============================================
// Example:Global Notification System(Context + useReducer)
// ============================================
// ---- 1. Create Context ----
const NotificationContext = createContext(null)
// ---- 2. Definition reducer ----
function notificationReducer(state, action) {
switch (action.type) {
case 'ADD':
return [...state, {
id: Date.now(),
message: action.message,
type: action.notifType || 'info',
read: false
}]
case 'MARK_READ':
return state.map(n =>
n.id === action.id ? { ...n, read: true } : n
)
case 'REMOVE':
return state.filter(n => n.id !== action.id)
case 'CLEAR_ALL':
return []
default:
return state
}
}
// ---- 3. Provider Components ----
function NotificationProvider({ children }) {
const [notifications, dispatch] = useReducer(notificationReducer, [])
// use useMemo Stable value
const value = useMemo(() => ({ notifications, dispatch }), [notifications])
return (
<NotificationContext.Provider value={value}>
{children}
</NotificationContext.Provider>
)
}
// ---- 4. Custom Hook ----
function useNotification() {
const context = useContext(NotificationContext)
if (!context) throw new Error('useNotification Must be NotificationProvider For internal use')
return context
}
// ---- 5. Using the notification component ----
function NotificationBell() {
const { notifications, dispatch } = useNotification()
const unread = notifications.filter(n => !n.read).length
return (
<div style={{ position: 'relative', cursor: 'pointer' }}>
🔔
{unread > 0 && (
<span style={{
position: 'absolute', top: '-8px', right: '-8px',
backgroundColor: '#ff4d4f', color: 'white',
borderRadius: '50%', width: '18px', height: '18px',
fontSize: '12px', display: 'flex', alignItems: 'center',
justifyContent: 'center'
}}>
{unread}
</span>
)}
</div>
)
}
function NotificationList() {
const { notifications, dispatch } = useNotification()
if (notifications.length === 0) {
return <p style={{ color: '#999', textAlign: 'center' }}>No notice at this time</p>
}
return (
<div style={{ maxWidth: '400px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '8px' }}>
<strong>Notice({notifications.length})</strong>
<button onClick={() => dispatch({ type: 'CLEAR_ALL' })} style={smallBtn}>Clear All</button>
</div>
{notifications.map(n => (
<div key={n.id} style={{
padding: '8px', marginBottom: '4px',
backgroundColor: n.read ? '#fafafa' : '#e6f7ff',
borderRadius: '4px', cursor: 'pointer',
display: 'flex', justifyContent: 'space-between',
alignItems: 'center'
}}
onClick={() => dispatch({ type: 'MARK_READ', id: n.id })}>
<span>
{n.type === 'error' ? '❌' : n.type === 'success' ? '✅' : 'ℹ️'}
{n.message}
</span>
<button onClick={e => { e.stopPropagation(); dispatch({ type: 'REMOVE', id: n.id }) }}
style={{ ...smallBtn, color: '#ff4d4f' }}>✕</button>
</div>
))}
</div>
)
}
// ---- The component that sends notifications ----
function NotificationSender() {
const { dispatch } = useNotification()
const [message, setMessage] = useState('')
function send(type) {
if (!message.trim()) return
dispatch({ type: 'ADD', message: message.trim(), notifType: type })
setMessage('')
}
return (
<div>
<input value={message} onChange={e => setMessage(e.target.value)}
placeholder="Notice Content" style={{ width: '100%', padding: '8px', marginBottom: '8px', border: '1px solid #d9d9d9', borderRadius: '4px' }} />
<div style={{ display: 'flex', gap: '4px' }}>
<button onClick={() => send('info')} style={typeBtn('#1890ff')}>ℹ️ info</button>
<button onClick={() => send('success')} style={typeBtn('#52c41a')}>✅ Success</button>
<button onClick={() => send('error')} style={typeBtn('#ff4d4f')}>❌ Error</button>
</div>
</div>
)
}
const smallBtn = { padding: '2px 8px', fontSize: '12px', border: 'none', backgroundColor: 'transparent', cursor: 'pointer' }
const typeBtn = (color) => ({ padding: '6px 12px', backgroundColor: color, color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer' })
// ---- 6. Assembly ----
function NotificationApp() {
return (
<NotificationProvider>
<div style={{ maxWidth: '500px', margin: '0 auto' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<h2>🔔 Notification System</h2>
<NotificationBell />
</div>
<NotificationSender />
<hr style={{ margin: '16px 0' }} />
<h3>List of Notices</h3>
<NotificationList />
</div>
</NotificationProvider>
)
}
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)
Output:
Displays: "Button". State: theme (setter: setTheme). Button: Button
// ============================================
// Example:User Authentication System——Context + Custom Hook Best Practices
// Features:Admin Login/Logged Out,Provide user information globally
// ============================================
import { createContext, useContext, useState, useCallback, useMemo } from 'react'
// ---- 1. Create Context ----
const AuthContext = createContext(null)
// ---- 2. Provider Components ----
function AuthProvider({ children }) {
const [user, setUser] = useState(null)
const [loading, setLoading] = useState(false)
const login = useCallback(async (username, password) => {
setLoading(true)
try {
// Simulate a login request
await new Promise(resolve => setTimeout(resolve, 800))
if (username && password) {
const loggedInUser = {
id: 1,
name: username,
email: `${username}@example.com`,
avatar: 'https://i.pravatar.cc/40',
role: username === 'admin' ? 'Administrator' : 'Regular User'
}
setUser(loggedInUser)
localStorage.setItem('user', JSON.stringify(loggedInUser))
return { success: true }
}
return { success: false, error: 'Incorrect username or password' }
} finally {
setLoading(false)
}
}, [])
const logout = useCallback(() => {
setUser(null)
localStorage.removeItem('user')
}, [])
// use useMemo Stable value Quote,Avoid Provider Render all child components again
const value = useMemo(() => ({
user,
loading,
isAuthenticated: !!user,
login,
logout
}), [user, loading, login, logout])
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
)
}
// ---- 3. Custom Hook + Error Checking ----
function useAuth() {
const context = useContext(AuthContext)
if (!context) {
throw new Error('useAuth Must be AuthProvider For internal use only')
}
return context
}
// ---- 4. Use Certified Components ----
function LoginForm() {
const { login, loading } = useAuth()
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState('')
async function handleSubmit(e) {
e.preventDefault()
setError('')
const result = await login(username, password)
if (!result.success) {
setError(result.error)
}
}
return (
<form onSubmit={handleSubmit} style={{ maxWidth: '400px', margin: '0 auto' }}>
<h2>🔐 Log In</h2>
{error && <p style={{ color: '#ff4d4f', fontSize: '14px' }}>{error}</p>}
<input value={username} onChange={e => setUsername(e.target.value)}
placeholder="Username(Input admin Experience the Role of an Administrator)"
style={{ width: '100%', padding: '8px', marginBottom: '8px', border: '1px solid #d9d9d9', borderRadius: '4px' }} />
<input type="password" value={password} onChange={e => setPassword(e.target.value)}
placeholder="Password"
style={{ width: '100%', padding: '8px', marginBottom: '8px', border: '1px solid #d9d9d9', borderRadius: '4px' }} />
<button type="submit" disabled={loading}
style={{ width: '100%', padding: '10px', backgroundColor: '#1890ff', color: 'white', border: 'none', borderRadius: '4px', cursor: loading ? 'not-allowed' : 'pointer' }}>
{loading ? 'Logging in......' : 'Log In'}
</button>
</form>
)
}
function UserDashboard() {
const { user, logout } = useAuth()
return (
<div style={{ maxWidth: '400px', margin: '0 auto' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '16px' }}>
<img src={user.avatar} alt={user.name}
style={{ width: '40px', height: '40px', borderRadius: '50%' }} />
<div>
<h3 style={{ margin: 0 }}>{user.name}</h3>
<p style={{ margin: '4px 0 0 0', color: '#666', fontSize: '13px' }}>
{user.email} · {user.role}
</p>
</div>
</div>
<button onClick={logout}
style={{ padding: '8px 16px', backgroundColor: '#ff4d4f', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer' }}>
Log Out
</button>
</div>
)
}
// ---- 5. Top-level component ----
function AuthApp() {
const { isAuthenticated } = useAuth()
return isAuthenticated ? <UserDashboard /> : <LoginForm />
}
// ---- 6. Entrance ----
function App() {
return (
<AuthProvider>
<div style={{ maxWidth: '600px', margin: '0 auto', padding: '20px' }}>
<AuthApp />
</div>
</AuthProvider>
)
}
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})
▶ Example 3: Combining Multiple Provider Layers with i18n Internationalization
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
const I18nContext = createContext(null)
function I18nProvider({ children }) {
const [locale, setLocale] = useState('zh')
const translations = {
zh: { title: 'App Title', greeting: 'Hello', logout: 'Exit' },
en: { title: 'App Title', greeting: 'Hello', logout: 'Logout' },
}
const t = useCallback((key) => translations[locale][key] || key, [locale])
const value = useMemo(() => ({ locale, setLocale, t }), [locale, t])
return <I18nContext.Provider value={value}>{children}</I18nContext.Provider>
}
function useI18n() {
const ctx = useContext(I18nContext)
if (!ctx) throw new Error('useI18n must be inside I18nProvider')
return ctx
}
function LocaleSwitcher() {
const { locale, setLocale } = useI18n()
return (
<select value={locale} onChange={e => setLocale(e.target.value)}
style={{ padding: '4px 8px', borderRadius: 4 }}>
<option value="zh">Chinese</option>
<option value="en">English</option>
</select>
)
}
function Greeting() {
const { t } = useI18n()
return <h2>{t('greeting')}</h2>
}
function I18nApp() {
return (
<I18nProvider>
<div style={{ maxWidth: 400, margin: '0 auto' }}>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<Greeting />
<LocaleSwitcher />
</div>
</div>
</I18nProvider>
)
}
Output:
Internationalization: language switcher changes all text. Context + provider pattern for multi-language support
▶ Example 4: Splitting the Context to Optimize Performance
Output:
Subheading: "{t('greeting')}". Displays: "Chinese". State: locale (setter: setLocale). Dropdown: Chinese, English. useMemo optimizes computation. useCallback memoizes handler. Context provides global state
const UserStateContext = createContext(null)
const UserDispatchContext = createContext(null)
function UserProvider({ children }) {
const [user, setUser] = useState(null)
const [loading, setLoading] = useState(false)
const dispatch = useMemo(() => ({
login: async (name) => {
setLoading(true)
await new Promise(r => setTimeout(r, 500))
setUser({ name, role: name === 'admin' ? 'Admin' : 'User' })
setLoading(false)
},
logout: () => setUser(null),
}), [])
const stateValue = useMemo(() => ({ user, loading }), [user, loading])
return (
<UserDispatchContext.Provider value={dispatch}>
<UserStateContext.Provider value={stateValue}>
{children}
</UserStateContext.Provider>
</UserDispatchContext.Provider>
)
}
function useUserState() { return useContext(UserStateContext) }
function useUserDispatch() { return useContext(UserDispatchContext) }
function LoginButton() {
const { login } = useUserDispatch()
const { loading } = useUserState()
return <button disabled={loading} onClick={() => login('Alice')}>Login</button>
}
function UserInfo() {
const { user } = useUserState()
if (!user) return <p>Not logged in</p>
return <p>Logged in as {user.name} ({user.role})</p>
}
function SplitContextApp() {
return (
<UserProvider>
<div style={{ maxWidth: 400, margin: '0 auto' }}>
<UserInfo />
<LoginButton />
</div>
</UserProvider>
)
}
Output:
SplitContextApp component with state: null, false. Renders interactive UI.
▶ Example 5: Complete Example of Using useContext as an Alternative to Props Drilling
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
const SettingsContext = createContext(null)
function SettingsProvider({ children }) {
const [settings, setSettings] = useState({
theme: 'light', fontSize: 14, sidebarCollapsed: false,
})
const updateSetting = useCallback((key, value) => {
setSettings(prev => ({ ...prev, [key]: value }))
}, [])
const value = useMemo(() => ({ settings, updateSetting }), [settings, updateSetting])
return <SettingsContext.Provider value={value}>{children}</SettingsContext.Provider>
}
function useSettings() {
const ctx = useContext(SettingsContext)
if (!ctx) throw new Error('useSettings must be inside SettingsProvider')
return ctx
}
function Toolbar() {
const { settings, updateSetting } = useSettings()
return (
<div style={{ padding: 8, background: settings.theme === 'dark' ? '#333' : '#f0f0f0', display: 'flex', gap: 8, alignItems: 'center' }}>
<button onClick={() => updateSetting('theme', settings.theme === 'dark' ? 'light' : 'dark')}>
{settings.theme === 'dark' ? '☀️' : '🌙'}
</button>
<button onClick={() => updateSetting('sidebarCollapsed', !settings.sidebarCollapsed)}>
{settings.sidebarCollapsed ? '▶' : '◀'} Sidebar
</button>
<select value={settings.fontSize} onChange={e => updateSetting('fontSize', Number(e.target.value))}>
<option value={12}>12px</option>
<option value={14}>14px</option>
<option value={16}>16px</option>
<option value={18}>18px</option>
</select>
</div>
)
}
function Content() {
const { settings } = useSettings()
return (
<div style={{ fontSize: settings.fontSize, color: settings.theme === 'dark' ? '#eee' : '#333',
background: settings.theme === 'dark' ? '#1a1a1a' : '#fff', padding: 16 }}>
<p>Current settings:</p>
<ul>
<li>Theme: {settings.theme}</li>
<li>Font size: {settings.fontSize}px</li>
<li>Sidebar: {settings.sidebarCollapsed ? 'collapsed' : 'expanded'}</li>
</ul>
</div>
)
}
function SettingsApp() {
return (
<SettingsProvider>
<div style={{ maxWidth: 600, margin: '0 auto' }}>
<Toolbar />
<Content />
</div>
</SettingsProvider>
)
}
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.