React Context API and Global State
1. createContext and Provider Components
Feature
Syntax
Description
Use Case
createContext
const Ctx = createContext(defaultValue)
Create context object
Define shared state container
Default Value
createContext(initialValue)
Fallback when no provider exists
Testing, development defaults
Provider Component
<Ctx.Provider value={...}>
Provide context value to children
Share data down component tree
Provider Value
value={{ state, actions }}
Object with state and updater functions
Complete state management
Nested Providers
<A><B><C /></B></A>
Stack multiple providers
Compose contexts
Provider Pattern
Wrap App with providers
Make context available globally
App-wide state
Example: Creating and providing context
import { createContext, useState } from 'react' ;
// 1. Create context with default value
const ThemeContext = createContext ({
theme: 'light' ,
toggleTheme : () => {}
});
// 2. Create provider component
export const ThemeProvider = ({ children }) => {
const [ theme , setTheme ] = useState ( 'light' );
const toggleTheme = () => {
setTheme ( prev => prev === 'light' ? 'dark' : 'light' );
};
const value = {
theme,
toggleTheme
};
return (
< ThemeContext.Provider value = {value}>
{children}
</ ThemeContext.Provider >
);
};
// 3. Create auth provider
const AuthContext = createContext ( null );
export const AuthProvider = ({ children }) => {
const [ user , setUser ] = useState ( null );
const [ loading , setLoading ] = useState ( true );
const login = async ( credentials ) => {
setLoading ( true );
const userData = await api. login (credentials);
setUser (userData);
setLoading ( false );
};
const logout = () => {
setUser ( null );
};
return (
< AuthContext.Provider value = {{ user, loading, login, logout }}>
{children}
</ AuthContext.Provider >
);
};
// 4. Compose providers in App
const App = () => (
< ThemeProvider >
< AuthProvider >
< Router >
< Layout />
</ Router >
</ AuthProvider >
</ ThemeProvider >
);
// 5. Provider composition helper
const ComposeProviders = ({ providers , children }) => {
return providers. reduceRight (
( acc , Provider ) => < Provider >{acc}</ Provider >,
children
);
};
// Usage
< ComposeProviders providers = {[ThemeProvider, AuthProvider, RouterProvider]}>
< App />
</ ComposeProviders >
2. useContext Hook and Context Consumption
Pattern
Syntax
Description
Best Practice
useContext Hook
const value = useContext(Context)
Access context value in component
Must be inside Provider
Destructure Value
const { state, action } = useContext(Ctx)
Extract specific values from context
Cleaner code, clear dependencies
Custom Hook
const useMyContext = () => useContext(Ctx)
Wrap useContext in custom hook
Better error handling, abstraction
Null Check
if (!context) throw Error
Ensure provider exists
Catch missing provider early
Multiple Contexts
Call useContext multiple times
Consume different contexts
Separate concerns
Example: Consuming context with custom hooks
import { createContext, useContext, useState } from 'react' ;
// Context setup
const UserContext = createContext ( null );
export const UserProvider = ({ children }) => {
const [ user , setUser ] = useState ( null );
return (
< UserContext.Provider value = {{ user, setUser }}>
{children}
</ UserContext.Provider >
);
};
// ✓ Good: Custom hook with error handling
export const useUser = () => {
const context = useContext (UserContext);
if ( ! context) {
throw new Error ( 'useUser must be used within UserProvider' );
}
return context;
};
// Usage in components
const Profile = () => {
const { user , setUser } = useUser ();
if ( ! user) return < Login />;
return (
< div >
< h1 >{user.name}</ h1 >
< button onClick = {() => setUser ( null )}>Logout</ button >
</ div >
);
};
// Multiple contexts example
const ThemeContext = createContext ( 'light' );
const LanguageContext = createContext ( 'en' );
const useTheme = () => useContext (ThemeContext);
const useLanguage = () => useContext (LanguageContext);
const Header = () => {
const theme = useTheme ();
const language = useLanguage ();
const { user } = useUser ();
return (
< header className = {theme}>
< span >{language === 'en' ? 'Welcome' : 'Bienvenido' }</ span >
{user && < span >{user.name}</ span >}
</ header >
);
};
// Selective context consumption
const Button = () => {
// Only subscribes to theme, not user
const theme = useTheme ();
return < button className = {theme}>Click</ button >;
};
// Pattern: Context + Reducer
const TodoContext = createContext ( null );
export const TodoProvider = ({ children }) => {
const [ state , dispatch ] = useReducer (todoReducer, initialState);
return (
< TodoContext.Provider value = {{ state, dispatch }}>
{children}
</ TodoContext.Provider >
);
};
export const useTodos = () => {
const context = useContext (TodoContext);
if ( ! context) throw new Error ( 'useTodos requires TodoProvider' );
return context;
};
3. Multiple Contexts and Context Composition
Pattern
Description
Benefit
Example
Separate Contexts
Different contexts for different concerns
Clear separation, selective updates
Theme, Auth, Language contexts
Nested Providers
Stack providers in component tree
Layer functionality
Auth wraps Theme wraps App
Context Composition
Combine multiple contexts in one hook
Convenient API, related data
useApp() returns theme + auth
Scoped Contexts
Context for specific feature area
Isolated state, no global pollution
Form context, Modal context
Context Hierarchy
Parent contexts provide to children
Natural data flow
App → Feature → Component
Example: Multiple contexts and composition
// Separate contexts for different concerns
const ThemeContext = createContext ( 'light' );
const AuthContext = createContext ( null );
const NotificationContext = createContext ( null );
// Provider setup
const App = () => (
< ThemeProvider >
< AuthProvider >
< NotificationProvider >
< Router />
</ NotificationProvider >
</ AuthProvider >
</ ThemeProvider >
);
// Composed hook for convenience
const useAppContext = () => {
const theme = useContext (ThemeContext);
const auth = useContext (AuthContext);
const notifications = useContext (NotificationContext);
return { theme, auth, notifications };
};
// Usage
const Dashboard = () => {
const { theme , auth , notifications } = useAppContext ();
return (
< div className = {theme}>
< h1 >Welcome {auth.user.name}</ h1 >
{notifications.messages. map ( msg => (
< div key = {msg.id}>{msg.text}</ div >
))}
</ div >
);
};
// Scoped context for features
const FormContext = createContext ( null );
const FormProvider = ({ children , initialValues }) => {
const [ values , setValues ] = useState (initialValues);
const [ errors , setErrors ] = useState ({});
const setValue = ( field , value ) => {
setValues ( prev => ({ ... prev, [field]: value }));
};
return (
< FormContext.Provider value = {{ values, errors, setValue }}>
{children}
</ FormContext.Provider >
);
};
// Form uses scoped context
const MyForm = () => (
< FormProvider initialValues = {{ name: '' , email: '' }}>
< FormFields />
< SubmitButton />
</ FormProvider >
);
// Hierarchical contexts
const AppContext = createContext ( null );
const FeatureContext = createContext ( null );
const FeatureArea = () => {
const appData = useContext (AppContext);
const [ featureState , setFeatureState ] = useState ({});
return (
< FeatureContext.Provider value = {{ appData, featureState, setFeatureState }}>
< FeatureComponents />
</ FeatureContext.Provider >
);
};
// Provider composition utility
const combineProviders = ( ... providers ) => {
return ({ children }) => {
return providers. reduce (
( acc , [ Provider , props ]) => (
< Provider { ... props}>{acc}</ Provider >
),
children
);
};
};
// Usage
const AppProviders = combineProviders (
[ThemeProvider, { defaultTheme: 'light' }],
[AuthProvider, {}],
[NotificationProvider, { position: 'top-right' }]
);
< AppProviders >
< App />
</ AppProviders >
4. Context Performance and Re-render Optimization
Issue
Cause
Solution
Pattern
Unnecessary Re-renders
Provider value changes on every render
Memoize provider value
useMemo(() => ({ state }), [state])
All Consumers Update
Any context change triggers all consumers
Split contexts by update frequency
Separate read-only from mutable state
Large Context Objects
Single context with many properties
Multiple smaller contexts
Theme, Auth, Settings as separate
Function References
New functions created every render
useCallback for updater functions
useCallback(() => {}, [])
Deep Component Trees
Many components between provider/consumer
React.memo on intermediate components
Prevent propagation of re-renders
Example: Context performance optimization
// ❌ Bad: New object every render
const BadProvider = ({ children }) => {
const [ count , setCount ] = useState ( 0 );
// This creates a new object on every render!
return (
< Context.Provider value = {{ count, setCount }}>
{children}
</ Context.Provider >
);
};
// ✓ Good: Memoized value
const GoodProvider = ({ children }) => {
const [ count , setCount ] = useState ( 0 );
const value = useMemo (
() => ({ count, setCount }),
[count] // Only recreate when count changes
);
return (
< Context.Provider value = {value}>
{children}
</ Context.Provider >
);
};
// ✓ Better: Split contexts
const CountContext = createContext ( 0 );
const CountDispatchContext = createContext ( null );
const CountProvider = ({ children }) => {
const [ count , setCount ] = useState ( 0 );
// State and dispatch are separate contexts
return (
< CountContext.Provider value = {count}>
< CountDispatchContext.Provider value = {setCount}>
{children}
</ CountDispatchContext.Provider >
</ CountContext.Provider >
);
};
// Components can subscribe to only what they need
const DisplayCount = () => {
const count = useContext (CountContext); // Only re-renders when count changes
return < div >{count}</ div >;
};
const IncrementButton = () => {
const setCount = useContext (CountDispatchContext); // Never re-renders!
return < button onClick = {() => setCount ( c => c + 1 )}>+</ button >;
};
// ✓ Good: Memoized callbacks
const UserProvider = ({ children }) => {
const [ user , setUser ] = useState ( null );
const login = useCallback ( async ( credentials ) => {
const userData = await api. login (credentials);
setUser (userData);
}, []);
const logout = useCallback (() => {
setUser ( null );
}, []);
const value = useMemo (
() => ({ user, login, logout }),
[user, login, logout]
);
return (
< UserContext.Provider value = {value}>
{children}
</ UserContext.Provider >
);
};
// Memo intermediate components
const Layout = memo (({ children }) => {
return (
< div className = "layout" >
< Sidebar />
< main >{children}</ main >
</ div >
);
});
// Split by update frequency
const StaticConfigContext = createContext ( null ); // Rarely changes
const DynamicDataContext = createContext ( null ); // Changes often
const App = () => (
< StaticConfigContext.Provider value = {config}>
< DynamicDataProvider >
< Routes />
</ DynamicDataProvider >
</ StaticConfigContext.Provider >
);
Warning: Context triggers re-renders in ALL consumers when value changes. Use
useMemo
for provider values, useCallback for functions, and split contexts to minimize unnecessary updates.
5. Context vs Prop Drilling Trade-offs
Approach
Pros
Cons
When to Use
Props
Explicit, type-safe, easy to trace, testable
Verbose with deep nesting, refactoring burden
2-3 levels deep, clear data flow needed
Context
Avoid drilling, cleaner intermediate components
Implicit dependencies, harder to trace, re-render issues
Deep nesting, many consumers, global state
Composition
Avoid both drilling and context, flexible
Requires component structure planning
Layout components, wrapper abstraction
State Management
Powerful features, DevTools, middleware
Boilerplate, learning curve, bundle size
Complex apps, time-travel debugging
URL State
Shareable, persistent, no drilling
Limited to serializable data, URL length
Filters, pagination, tabs
Example: Comparing approaches
// Approach 1: Props (explicit but verbose)
const App = () => {
const [ theme , setTheme ] = useState ( 'light' );
return < Layout theme = {theme} setTheme = {setTheme} />;
};
const Layout = ({ theme , setTheme }) => (
< div >
< Header theme = {theme} setTheme = {setTheme} />
< Content theme = {theme} />
</ div >
);
const Header = ({ theme , setTheme }) => (
< header >
< ThemeToggle theme = {theme} setTheme = {setTheme} />
</ header >
);
// Approach 2: Context (implicit but cleaner)
const ThemeContext = createContext ( 'light' );
const App = () => {
const [ theme , setTheme ] = useState ( 'light' );
return (
< ThemeContext.Provider value = {{ theme, setTheme }}>
< Layout />
</ ThemeContext.Provider >
);
};
const Layout = () => (
< div >
< Header />
< Content />
</ div >
);
const ThemeToggle = () => {
const { theme , setTheme } = useContext (ThemeContext);
return < button onClick = {() => setTheme (theme === 'light' ? 'dark' : 'light' )}>Toggle</ button >;
};
// Approach 3: Composition (flexible)
const App = () => {
const [ theme , setTheme ] = useState ( 'light' );
return (
< Layout
header = {< Header theme = {theme} setTheme = {setTheme} />}
content = {< Content theme = {theme} />}
/>
);
};
const Layout = ({ header , content }) => (
< div >
{header}
{content}
</ div >
);
// Decision guide:
// Props: 1-3 levels, clear data flow, explicit dependencies
// Context: 4+ levels, many consumers, truly global state
// Composition: Layout components, avoid wrapper drilling
// State library: Complex state, time-travel, middleware needed
Note: Start with props. Add context when drilling becomes painful (usually 4+ levels). Consider
composition before reaching for context. Reserve state management libraries for truly complex apps.
6. Custom Context Hooks and Abstractions
Pattern
Implementation
Benefit
Example
Custom Hook
Wrap useContext in function
Better error messages, validation
useAuth() instead of useContext(AuthContext)
Error Handling
Throw if provider missing
Catch mistakes early
Better DX, clear error messages
Selector Pattern
Custom hooks for subsets of context
Optimized updates, clearer API
useUserName() only subscribes to name
Action Creators
Provide functions not raw dispatch
Type-safe, encapsulated logic
login() instead of dispatch({ type: 'LOGIN' })
Computed Values
Derive values in custom hooks
Reusable logic, memoization
useIsAuthenticated() derives from user state
Example: Custom context hooks and abstractions
// Advanced context pattern with custom hooks
const AuthContext = createContext ( null );
// Provider with reducer
export const AuthProvider = ({ children }) => {
const [ state , dispatch ] = useReducer (authReducer, {
user: null ,
loading: true ,
error: null
});
const value = useMemo (() => ({ state, dispatch }), [state]);
return (
< AuthContext.Provider value = {value}>
{children}
</ AuthContext.Provider >
);
};
// Base hook with error handling
const useAuthContext = () => {
const context = useContext (AuthContext);
if ( ! context) {
throw new Error ( 'useAuth hooks must be used within AuthProvider' );
}
return context;
};
// Public API - action creators
export const useAuth = () => {
const { state , dispatch } = useAuthContext ();
const login = useCallback ( async ( credentials ) => {
dispatch ({ type: 'LOGIN_START' });
try {
const user = await api. login (credentials);
dispatch ({ type: 'LOGIN_SUCCESS' , payload: user });
} catch (error) {
dispatch ({ type: 'LOGIN_ERROR' , payload: error.message });
}
}, [dispatch]);
const logout = useCallback (() => {
dispatch ({ type: 'LOGOUT' });
}, [dispatch]);
return {
user: state.user,
loading: state.loading,
error: state.error,
login,
logout
};
};
// Selector hooks for performance
export const useUser = () => {
const { state } = useAuthContext ();
return state.user;
};
export const useIsAuthenticated = () => {
const { state } = useAuthContext ();
return state.user !== null ;
};
export const useAuthLoading = () => {
const { state } = useAuthContext ();
return state.loading;
};
// Usage in components
const Profile = () => {
const { user , logout } = useAuth ();
return (
< div >
< h1 >{user.name}</ h1 >
< button onClick = {logout}>Logout</ button >
</ div >
);
};
const LoginButton = () => {
const isAuthenticated = useIsAuthenticated ();
const { login } = useAuth ();
if (isAuthenticated) return null ;
return < button onClick = {() => login ({ email, password })}>Login</ button >;
};
// Advanced: Factory for creating context hooks
const createContextHook = ( Context , name ) => {
return () => {
const context = useContext (Context);
if ( ! context) {
throw new Error (\ `use \$ {name} must be used within \$ {name}Provider \` );
}
return context;
};
};
// Usage
const useTheme = createContextHook(ThemeContext, 'Theme');
const useSettings = createContextHook(SettingsContext, 'Settings');
Context API Best Practices
Custom hooks : Always wrap useContext in custom hooks with error handling
Memoization : Use useMemo for provider values and useCallback for functions
Split contexts : Separate by concern and update frequency to optimize
re-renders
Action creators : Provide high-level APIs, hide implementation details
Start simple : Use props first, context for deep drilling (4+ levels)
Composition over context : Consider component composition before context