Default Value: The default value in createContext(defaultValue) is only used when
a component doesn't have a matching Provider above it in the tree. It's useful for testing components in
isolation.
import { createContext, useContext, useState } from 'react';const AuthContext = createContext(null);// Providerexport function AuthProvider({ children }) { const [user, setUser] = useState(null); const [isAuthenticated, setIsAuthenticated] = useState(false); const login = (credentials) => { // Login logic setUser({ name: credentials.username }); setIsAuthenticated(true); }; const logout = () => { setUser(null); setIsAuthenticated(false); }; const value = { user, isAuthenticated, login, logout }; return ( <AuthContext.Provider value={value}> {children} </AuthContext.Provider> );}// Custom hook with validationexport function useAuth() { const context = useContext(AuthContext); if (context === null) { throw new Error('useAuth must be used within AuthProvider'); } return context;}// Usage in componentfunction UserProfile() { const { user, logout } = useAuth(); // Safe, validated access return ( <div> <p>Welcome, {user.name}</p> <button onClick={logout}>Logout</button> </div> );}
Warning: Components using useContext will re-render when the context value
changes. This happens even if the component only uses a small part of the context value. See section 3.4 for
optimization techniques.
3. Multiple Context Providers and Context Composition
Pattern
Implementation
Use Case
Nested Providers
Stack multiple Providers in tree hierarchy
Different concerns (theme, auth, settings, etc.)
Composed Provider
Single component wrapping multiple Providers
Reduce nesting, cleaner App component
Dependent Contexts
One context consumes another context's value
Context depends on data from another context
Composition Strategy
Code Pattern
Pros/Cons
Manual Nesting
<A><B><C>{children}</C></B></A>
✅ Explicit, clear dependencies ❌ Verbose, deep nesting
Provider Composer
<ComposeProviders providers={[A, B, C]}>
✅ Clean, scalable ❌ Extra abstraction
Single Root Provider
<AppProvider> wraps all contexts
✅ Single import, organized ❌ All contexts loaded together
// UserPreferences context depends on AuthContextfunction UserPreferencesProvider({ children }) { const { user } = useAuth(); // Depends on AuthContext const [preferences, setPreferences] = useState(null); useEffect(() => { if (user) { // Load user preferences when user is available loadPreferences(user.id).then(setPreferences); } else { setPreferences(null); } }, [user]); const value = { preferences, setPreferences }; return ( <PreferencesContext.Provider value={value}> {children} </PreferencesContext.Provider> );}// Must be nested under AuthProviderfunction App() { return ( <AuthProvider> <UserPreferencesProvider> <Dashboard /> </UserPreferencesProvider> </AuthProvider> );}
4. Context Performance Optimization Techniques
Problem
Cause
Solution
Unnecessary Re-renders
Context value changes, all consumers re-render
Split contexts, memoize value, use selectors
New Object Every Render
Provider creates new value object on each render
Memoize context value with useMemo
Consuming Entire Context
Component re-renders even if it uses one field
Split context by concern, use selector pattern
Optimization Technique
Implementation
Benefit
Memoize Context Value ESSENTIAL
const value = useMemo(() => ({...}), [deps])
Prevent re-renders from object identity changes
Split Context by Concern
Separate contexts for data and updaters
Components only re-render when needed data changes
React.memo on Consumers
export default React.memo(Component)
Prevent re-renders from parent updates
Selector Pattern
Custom hook with selector function
Subscribe to specific slice of context state
Separate State/Dispatch Contexts
One context for state, another for dispatch
Components using only dispatch never re-render
Example: Memoizing context value
import { createContext, useState, useMemo } from 'react';// ❌ Bad: New object every render, causes unnecessary re-rendersfunction BadProvider({ children }) { const [count, setCount] = useState(0); const [name, setName] = useState(''); // New object reference on every render! const value = { count, setCount, name, setName }; return <MyContext.Provider value={value}>{children}</MyContext.Provider>;}// ✅ Good: Memoized value, only changes when dependencies changefunction GoodProvider({ children }) { const [count, setCount] = useState(0); const [name, setName] = useState(''); // Memoized - same object reference unless count or name changes const value = useMemo( () => ({ count, setCount, name, setName }), [count, name] ); return <MyContext.Provider value={value}>{children}</MyContext.Provider>;}
Example: Split context for state and dispatch
import { createContext, useContext, useReducer, useMemo } from 'react';// Separate contexts for state and dispatchconst TodoStateContext = createContext(null);const TodoDispatchContext = createContext(null);function TodoProvider({ children }) { const [state, dispatch] = useReducer(todoReducer, initialState); // State context value - changes when state changes const stateValue = useMemo(() => state, [state]); // Dispatch context value - never changes (dispatch is stable) const dispatchValue = useMemo(() => dispatch, []); return ( <TodoStateContext.Provider value={stateValue}> <TodoDispatchContext.Provider value={dispatchValue}> {children} </TodoDispatchContext.Provider> </TodoStateContext.Provider> );}// Hooks for consumingexport function useTodoState() { const context = useContext(TodoStateContext); if (!context) throw new Error('Must be used within TodoProvider'); return context;}export function useTodoDispatch() { const context = useContext(TodoDispatchContext); if (!context) throw new Error('Must be used within TodoProvider'); return context;}// Usage: Components only using dispatch never re-render when state changesfunction AddTodoButton() { const dispatch = useTodoDispatch(); // Won't re-render on state changes return ( <button onClick={() => dispatch({ type: 'ADD', payload: 'New Todo' })}> Add Todo </button> );}// Component using state re-renders when state changesfunction TodoList() { const { todos } = useTodoState(); // Re-renders when state changes return <ul>{todos.map(t => <li key={t.id}>{t.text}</li>)}</ul>;}
Example: Selector pattern for granular subscriptions
// Custom hook with selectorfunction useStoreSelector(selector) { const store = useContext(StoreContext); return selector(store);}// Usage: Subscribe to specific datafunction UserProfile() { // Only re-renders when user changes, not when cart or settings change const user = useStoreSelector(store => store.user); return <div>{user.name}</div>;}function CartCount() { // Only re-renders when cart items change const itemCount = useStoreSelector(store => store.cart.items.length); return <span>{itemCount} items</span>;}
Performance Tip: For high-frequency updates, consider external state management libraries like
Zustand or Jotai that have built-in selector optimizations, or use useSyncExternalStore for
external stores.