React Context API - State Sharing
1. createContext and Provider Component Patterns (React.createContext)
| API | Syntax | Description | Return Value |
|---|---|---|---|
| createContext | createContext(defaultValue) |
Creates a Context object with default value | Context object with Provider and Consumer |
| Context.Provider | <MyContext.Provider value={value}> |
Provides context value to child components | Accepts value prop, passes to consumers |
| Context.Consumer | <MyContext.Consumer>{value => ...}</MyContext.Consumer> |
Render prop pattern for consuming context | Legacy - use useContext hook instead |
| Provider Pattern | Implementation | Use Case |
|---|---|---|
| Simple Provider | <Context.Provider value={state}>{children}</Context.Provider> |
Pass static or computed value directly |
| Provider Component RECOMMENDED | Wrap Provider in custom component with state management | Encapsulate state logic, cleaner API for consumers |
| Provider with useState | Provider component manages state with useState hook | Simple global state, few update operations |
| Provider with useReducer | Provider component manages state with useReducer | Complex state logic, many actions, Redux-like |
Example: Basic Context creation and Provider setup
import { createContext, useState } from 'react';
// 1. Create Context with default value
const ThemeContext = createContext('light');
// 2. Create Provider component
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
// Value object with state and updaters
const value = { theme, toggleTheme };
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}
// 3. Usage: Wrap app with Provider
function App() {
return (
<ThemeProvider>
<Header />
<Main />
<Footer />
</ThemeProvider>
);
}
Example: Provider with useReducer for complex state
import { createContext, useReducer } from 'react';
// Context
const CartContext = createContext(null);
// Reducer
function cartReducer(state, action) {
switch (action.type) {
case 'ADD_ITEM':
return { ...state, items: [...state.items, action.payload] };
case 'REMOVE_ITEM':
return { ...state, items: state.items.filter(i => i.id !== action.payload) };
case 'UPDATE_QUANTITY':
return {
...state,
items: state.items.map(i =>
i.id === action.payload.id ? { ...i, quantity: action.payload.quantity } : i
)
};
case 'CLEAR':
return { ...state, items: [] };
default:
return state;
}
}
// Provider component
function CartProvider({ children }) {
const [state, dispatch] = useReducer(cartReducer, { items: [], total: 0 });
// Derived values
const itemCount = state.items.reduce((sum, item) => sum + item.quantity, 0);
const totalPrice = state.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
const value = {
items: state.items,
itemCount,
totalPrice,
dispatch
};
return (
<CartContext.Provider value={value}>
{children}
</CartContext.Provider>
);
}
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.
2. useContext Hook and Context Consumption (React.useContext)
| API | Syntax | Description | Return Value |
|---|---|---|---|
| useContext | useContext(MyContext) |
Subscribes component to context changes | Current context value from nearest Provider |
| Context Argument | Pass Context object (not Provider/Consumer) | Must be the Context object from createContext |
Returns value prop from nearest Provider |
| Consumption Pattern | Implementation | Benefit |
|---|---|---|
| Direct useContext | const value = useContext(MyContext) |
Simple, direct access to context |
| Custom Hook RECOMMENDED | const useMyContext = () => useContext(MyContext) |
Encapsulation, validation, better API |
| Selective Access | const { user, setUser } = useContext(UserContext) |
Destructure only needed values from context |
Example: Consuming context with useContext
import { useContext } from 'react';
// Consume context in component
function ThemeToggle() {
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<button onClick={toggleTheme}>
Current theme: {theme}
</button>
);
}
// Another consumer
function ThemedPanel() {
const { theme } = useContext(ThemeContext);
const styles = {
background: theme === 'light' ? '#fff' : '#333',
color: theme === 'light' ? '#333' : '#fff'
};
return <div style={styles}>Themed content</div>;
}
Example: Custom hook for context with validation
import { createContext, useContext, useState } from 'react';
const AuthContext = createContext(null);
// Provider
export 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 validation
export function useAuth() {
const context = useContext(AuthContext);
if (context === null) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
}
// Usage in component
function 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 |
Example: Multiple providers composition
// Manual nesting (simple but verbose)
function App() {
return (
<ThemeProvider>
<AuthProvider>
<LanguageProvider>
<NotificationProvider>
<Routes />
</NotificationProvider>
</LanguageProvider>
</AuthProvider>
</ThemeProvider>
);
}
// Better: Compose providers helper
function ComposeProviders({ providers, children }) {
return providers.reduceRight((acc, Provider) => {
return <Provider>{acc}</Provider>;
}, children);
}
function App() {
return (
<ComposeProviders providers={[
ThemeProvider,
AuthProvider,
LanguageProvider,
NotificationProvider
]}>
<Routes />
</ComposeProviders>
);
}
// Best: Single root provider
function AppProvider({ children }) {
return (
<ThemeProvider>
<AuthProvider>
<LanguageProvider>
<NotificationProvider>
{children}
</NotificationProvider>
</LanguageProvider>
</AuthProvider>
</ThemeProvider>
);
}
function App() {
return (
<AppProvider>
<Routes />
</AppProvider>
);
}
Example: Dependent contexts
// UserPreferences context depends on AuthContext
function 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 AuthProvider
function 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-renders
function 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 change
function 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 dispatch
const 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 consuming
export 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 changes
function 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 changes
function 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 selector
function useStoreSelector(selector) {
const store = useContext(StoreContext);
return selector(store);
}
// Usage: Subscribe to specific data
function 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.
5. Context vs Prop Drilling - When to Use Which
| Aspect | Prop Drilling | Context API |
|---|---|---|
| Explicitness | ✅ Explicit data flow, easy to trace | ❌ Implicit, harder to track dependencies |
| Boilerplate | ❌ Pass props through many components | ✅ Minimal, direct access anywhere |
| Reusability | ✅ Components are portable, self-contained | ❌ Components coupled to context structure |
| Performance | ✅ Optimal, only affected components re-render | ⚠️ All consumers re-render on context change |
| Testing | ✅ Easy to test, pass props directly | ❌ Must wrap in Provider, mock context |
| Refactoring | ❌ Difficult to change prop names/structure | ✅ Change context, consumers auto-update |
| Use Prop Drilling When | Use Context When |
|---|---|
| 2-3 levels of component nesting | 4+ levels of component nesting |
| Props change frequently | Props change infrequently (theme, locale, auth) |
| Component reusability is priority | Developer experience is priority |
| Data is specific to component tree branch | Data is truly global (app-wide) |
| Simple, small applications | Medium to large applications |
| Performance is critical | Convenience outweighs performance concerns |
Example: When prop drilling is acceptable
// Acceptable: 2-3 levels, clear data flow
function App() {
const [user, setUser] = useState(null);
return <Dashboard user={user} setUser={setUser} />;
}
function Dashboard({ user, setUser }) {
return <UserProfile user={user} setUser={setUser} />;
}
function UserProfile({ user, setUser }) {
return <div>{user.name}</div>;
}
// Problematic: 5+ levels, props only used at the end
function App() {
const [theme, setTheme] = useState('light');
return <Layout theme={theme} setTheme={setTheme} />;
}
function Layout({ theme, setTheme }) {
return <Sidebar theme={theme} setTheme={setTheme} />;
}
function Sidebar({ theme, setTheme }) {
return <Menu theme={theme} setTheme={setTheme} />;
}
function Menu({ theme, setTheme }) {
return <MenuItem theme={theme} setTheme={setTheme} />;
}
function MenuItem({ theme, setTheme }) {
// Finally used here, but passed through 4 intermediate components
return <button style={{ background: theme }}>Item</button>;
}
Decision Framework
- Start with props - simpler, more explicit
- Move to Context when props pass through 4+ components
- Use Context for: theme, locale, auth, global UI state
- Avoid Context for: frequently changing data, component-specific state
- Hybrid approach: Context for global, props for local tree data
6. Context Testing and Development Patterns
| Testing Pattern | Implementation | Use Case |
|---|---|---|
| Wrap in Provider | Use real Provider with test values | Integration tests, test Provider + Consumer |
| Custom Render Wrapper | Create test utility wrapping component in Providers | Avoid repeating Provider setup in every test |
| Mock Context Value | Create mock Provider with controlled values | Test component with specific context states |
| Test Provider Logic | Test Provider component's state management separately | Unit test context logic without consumers |
| Development Pattern | Description | Benefit |
|---|---|---|
| Context DevTools | Use React DevTools to inspect context values | Debug context state and changes |
| Context Logger | Add logging to Provider for state changes | Track when and why context updates |
| TypeScript Inference | Strongly type context for autocomplete and safety | Catch errors at compile time, better DX |
| Default Value Warning | Throw error in custom hook if context is null | Catch missing Provider early in development |
Example: Testing components with Context
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
// Test with real Provider
test('ThemeToggle changes theme', async () => {
render(
<ThemeProvider>
<ThemeToggle />
</ThemeProvider>
);
const button = screen.getByRole('button');
expect(button).toHaveTextContent('light');
await userEvent.click(button);
expect(button).toHaveTextContent('dark');
});
// Custom render wrapper for multiple providers
function renderWithProviders(ui, { themeValue = 'light', ...options } = {}) {
function Wrapper({ children }) {
return (
<ThemeProvider initialTheme={themeValue}>
<AuthProvider>
<LanguageProvider>
{children}
</LanguageProvider>
</AuthProvider>
</ThemeProvider>
);
}
return render(ui, { wrapper: Wrapper, ...options });
}
// Usage with custom wrapper
test('component renders with dark theme', () => {
renderWithProviders(<MyComponent />, { themeValue: 'dark' });
// Test component with dark theme
});
Example: Mock context for testing
// Mock Provider for testing
function MockAuthProvider({ children, value }) {
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
}
test('shows logout button when authenticated', () => {
const mockValue = {
user: { name: 'John', id: 1 },
isAuthenticated: true,
logout: jest.fn()
};
render(
<MockAuthProvider value={mockValue}>
<UserMenu />
</MockAuthProvider>
);
expect(screen.getByText('Logout')).toBeInTheDocument();
});
test('shows login button when not authenticated', () => {
const mockValue = {
user: null,
isAuthenticated: false,
login: jest.fn()
};
render(
<MockAuthProvider value={mockValue}>
<UserMenu />
</MockAuthProvider>
);
expect(screen.getByText('Login')).toBeInTheDocument();
});
Example: TypeScript context with full type safety
import { createContext, useContext, ReactNode } from 'react';
// Define types
interface User {
id: number;
name: string;
email: string;
}
interface AuthContextType {
user: User | null;
isAuthenticated: boolean;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
}
// Create typed context (null as default indicates provider required)
const AuthContext = createContext<AuthContextType | null>(null);
// Provider
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const login = async (email: string, password: string) => {
const userData = await loginAPI(email, password);
setUser(userData);
};
const logout = () => setUser(null);
const value: AuthContextType = {
user,
isAuthenticated: user !== null,
login,
logout
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
// Typed hook with runtime validation
export function useAuth(): AuthContextType {
const context = useContext(AuthContext);
if (context === null) {
throw new Error('useAuth must be used within AuthProvider');
}
return context; // TypeScript knows this is AuthContextType
}
// Usage with full type safety
function Profile() {
const { user, logout } = useAuth(); // Fully typed, autocomplete works
if (!user) return null;
return (
<div>
<h1>{user.name}</h1> {/* TypeScript knows user.name exists */}
<p>{user.email}</p>
<button onClick={logout}>Logout</button>
</div>
);
}
Context API Best Practices Summary
- Create custom hooks with validation for consuming context
- Always memoize context values with useMemo to prevent unnecessary re-renders
- Split contexts by concern (state/dispatch, read/write) for performance
- Use TypeScript for type-safe context and better developer experience
- Prefer props for 2-3 levels, Context for 4+ levels or truly global state
- Create custom render wrappers for testing components with context
- Compose providers for cleaner app structure with multiple contexts