React Context API and Global State
1. createContext and Provider Pattern
| 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 Consuming Context
| 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 Patterns
| 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 API vs Prop Drilling - When to Use
| 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 Pattern
| 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