Ways to Consume Context
Once you've created a context and set up a provider, the next step is to access that context in your components. React offers a few different ways to consume context:
In modern React development, the useContext hook is the preferred method for consuming context in functional components.
The useContext Hook
The useContext hook is the most straightforward way to consume context in functional components. It accepts a context object and returns the current context value for that context.
import { useContext } from 'react';
import { ThemeContext } from './ThemeContext';
function ThemedButton() {
// Use the useContext hook to access the context value
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<button
style={{
backgroundColor: theme === 'dark' ? '#333' : '#fff',
color: theme === 'dark' ? '#fff' : '#333',
padding: '8px 16px',
border: '1px solid #ccc',
borderRadius: '4px'
}}
onClick={toggleTheme}
>
Toggle Theme
</button>
);
}
The useContext hook provides these benefits:
- Simple, clean syntax without nested callbacks
- Works with React's rules of hooks for consistency
- Makes context consumption look like any other hook in your component
- Enables destructuring of context values for convenient access
Real-World Analogy: Radio Tuning
Consuming context with useContext is like tuning into a radio station. The context is like a radio frequency that's broadcasting information, and useContext is like your radio receiver tuned to that specific frequency.
Just as multiple radios can tune into the same station, multiple components can use useContext to access the same context. And just as you don't need to run a wire from the radio station to your radio, components don't need direct prop connections to access context.
Consuming Multiple Contexts
In real applications, you'll often need to consume multiple contexts within a single component. With useContext, this is straightforward:
import { useContext } from 'react';
import { ThemeContext } from './ThemeContext';
import { UserContext } from './UserContext';
import { LanguageContext } from './LanguageContext';
function ProfileCard() {
// Consume multiple contexts
const { theme } = useContext(ThemeContext);
const { user } = useContext(UserContext);
const { translations } = useContext(LanguageContext);
if (!user) return <div>{translations.loading}...</div>;
return (
<div className={`profile-card ${theme}`}>
<h2>{translations.greeting}, {user.name}!</h2>
<p>{translations.memberSince}: {new Date(user.joinDate).toLocaleDateString()}</p>
<button>{translations.editProfile}</button>
</div>
);
}
This approach is much cleaner than nesting multiple Context.Consumer components, which was the approach before hooks:
// Before hooks - more verbose and harder to read
function ProfileCard() {
return (
<ThemeContext.Consumer>
{({ theme }) => (
<UserContext.Consumer>
{({ user }) => (
<LanguageContext.Consumer>
{({ translations }) => (
<div className={`profile-card ${theme}`}>
<h2>{translations.greeting}, {user.name}!</h2>
{/* Rest of component */}
</div>
)}
</LanguageContext.Consumer>
)}
</UserContext.Consumer>
)}
</ThemeContext.Consumer>
);
}
Creating Custom Context Hooks
A best practice for consuming context is to create custom hooks that encapsulate the useContext call and provide a more semantic API. This approach offers several benefits:
- Hides the implementation details of how the context is consumed
- Provides a more domain-specific name that describes what the context is for
- Allows for adding validation, default values, or derived values
- Makes it easier to change the context implementation without affecting consumers
- Improves code reusability across components
// themeContext.js
import { createContext, useContext, useState } from 'react';
const ThemeContext = createContext();
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, setTheme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
// Custom hook for consuming the theme context
export function useTheme() {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}
Now, components can use the custom hook instead of directly using useContext:
import { useTheme } from './themeContext';
function ThemedButton() {
// Use the custom hook instead of useContext
const { theme, toggleTheme } = useTheme();
return (
<button
style={{
backgroundColor: theme === 'dark' ? '#333' : '#fff',
color: theme === 'dark' ? '#fff' : '#333'
}}
onClick={toggleTheme}
>
Toggle Theme
</button>
);
}
Real-World Example: E-commerce Product Page
In an e-commerce application, you might have custom hooks like:
useCart()- For accessing and manipulating the shopping cartuseAuth()- For handling user authenticationuseWishlist()- For managing user wishlist items
A product detail page could use these hooks to implement "Add to Cart" and "Add to Wishlist" functionality without knowing how these features are implemented behind the scenes.
function ProductDetailPage({ productId }) {
const { product, loading } = useProduct(productId);
const { addToCart, isInCart } = useCart();
const { addToWishlist, isInWishlist } = useWishlist();
const { isAuthenticated } = useAuth();
if (loading) return <LoadingSpinner />;
return (
<div className="product-detail">
<h1>{product.name}</h1>
<p>${product.price}</p>
<button
onClick={() => addToCart(product)}
disabled={isInCart(product.id)}
>
{isInCart(product.id) ? 'Added to Cart' : 'Add to Cart'}
</button>
{isAuthenticated && (
<button
onClick={() => addToWishlist(product)}
disabled={isInWishlist(product.id)}
>
{isInWishlist(product.id) ? 'In Wishlist' : 'Add to Wishlist'}
</button>
)}
</div>
);
}
Context Selectors for Performance
One challenge with consuming context is that any update to the context value causes all components that use that context to re-render, even if they only use a portion of the context. Context selectors help mitigate this issue:
// Without selectors - will re-render for any user context change
function UserGreeting() {
const { user } = useContext(UserContext);
return <h1>Hello, {user.name}!</h1>;
}
// With selectors - only re-renders when user.name changes
function UserGreeting() {
const userName = useUserName(); // Custom hook that only selects the name
return <h1>Hello, {userName}!</h1>;
}
// Implementation of the selector hook
function useUserName() {
const { user } = useContext(UserContext);
return user.name;
}
For more complex scenarios, you can use libraries like use-context-selector or implement your own memoization logic:
import { useState, useContext, useEffect, useRef } from 'react';
// Custom hook that only re-renders when the selected value changes
function useContextSelector(context, selector) {
const contextValue = useContext(context);
const [selectedValue, setSelectedValue] = useState(() =>
selector(contextValue)
);
const prevValueRef = useRef(selectedValue);
const selectorRef = useRef(selector);
// Update selector ref when it changes
useEffect(() => {
selectorRef.current = selector;
}, [selector]);
// Update selected value when context or selector changes
useEffect(() => {
const newValue = selectorRef.current(contextValue);
if (!Object.is(prevValueRef.current, newValue)) {
setSelectedValue(newValue);
prevValueRef.current = newValue;
}
}, [contextValue]);
return selectedValue;
}
// Usage
function UserAvatar() {
const avatarUrl = useContextSelector(
UserContext,
user => user.profile.avatarUrl
);
return <img src={avatarUrl} alt="User avatar" />;
}
This technique ensures that components only re-render when the specific part of the context they care about changes, improving performance in large applications.
Error Handling When Consuming Context
When a component tries to consume a context but there's no matching provider in the component tree above it, React will use the default value that was passed to createContext(). This can lead to confusion if not handled properly.
A best practice is to add checks in your custom hooks to ensure the context is being used within a provider:
export function useAuth() {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}
This early error detection helps catch mistakes during development instead of producing subtle bugs at runtime.
Common Context Consumer Errors
- Missing Provider: Using context without a provider in the component tree
- Provider Order: Incorrect nesting order when using multiple contexts
- Value Structure: Accessing properties that don't exist in the context value
- Default Value Confusion: Relying on default values incorrectly
Advanced Context Consumer Patterns
1. Conditional Context Value Usage
Sometimes you want components to adapt based on whether a context is available:
function FeatureFlag({ children, fallback }) {
// Will use the default value (undefined) if no provider exists
const features = useContext(FeatureContext);
// If the context isn't available, render the fallback
if (!features) {
return fallback ? fallback : null;
}
// Render children with the features object as a prop
return children(features);
}
// Usage
function App() {
return (
<div>
<FeatureFlag
fallback={<p>Premium features not available.</p>}
>
{features => (
<div>
{features.premium && <PremiumFeature />}
{features.beta && <BetaFeature />}
</div>
)}
</FeatureFlag>
</div>
);
}
2. Dynamic Context Selection
In some cases, you might want to choose which context to use at runtime:
function DynamicTheme({ theme, children }) {
// Select which theme context to use based on a prop
const ThemeContext = theme === 'admin'
? AdminThemeContext
: UserThemeContext;
return (
<ThemeContext.Consumer>
{themeValue => children(themeValue)}
</ThemeContext.Consumer>
);
}
// Usage
function App({ userType }) {
return (
<DynamicTheme theme={userType}>
{theme => (
<div style={{ background: theme.background, color: theme.text }}>
Content with dynamic theme
</div>
)}
</DynamicTheme>
);
}
3. Higher-Order Components for Context
While hooks are preferred in functional components, you might still need to use Higher-Order Components (HOCs) to connect class components to context:
// withTheme.js - HOC for connecting components to theme context
function withTheme(Component) {
return function WithThemeComponent(props) {
// Get the theme using the hook
const theme = useTheme();
// Pass the theme and original props to the wrapped component
return <Component {...props} theme={theme} />;
};
}
// Usage with a class component
class ThemedButton extends React.Component {
render() {
const { theme, ...rest } = this.props;
return (
<button
style={{
backgroundColor: theme.background,
color: theme.text
}}
{...rest}
>
{this.props.children}
</button>
);
}
}
// Connect the component to the theme context
const ConnectedThemedButton = withTheme(ThemedButton);
Testing Components that Consume Context
When testing components that consume context, you need to ensure they have the expected context value available:
1. Wrapping with Providers in Tests
// Button.test.js
import { render, screen, fireEvent } from '@testing-library/react';
import Button from './Button';
import { ThemeProvider } from './ThemeContext';
test('Button changes style based on theme', () => {
render(
<ThemeProvider>
<Button>Click Me</Button>
</ThemeProvider>
);
const button = screen.getByRole('button', { name: /click me/i });
// Test initial state (light theme)
expect(button).toHaveStyle({
backgroundColor: '#fff',
color: '#333'
});
// Find and click the theme toggle button
const themeToggle = screen.getByRole('button', { name: /toggle theme/i });
fireEvent.click(themeToggle);
// Test dark theme
expect(button).toHaveStyle({
backgroundColor: '#333',
color: '#fff'
});
});
2. Creating Test Providers with Controlled Values
// testUtils.js
import { ThemeContext } from './ThemeContext';
export function renderWithTheme(ui, { theme = 'light', toggleTheme = jest.fn() } = {}) {
return render(
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{ui}
</ThemeContext.Provider>
);
}
// Usage in tests
test('Button has dark theme styles', () => {
renderWithTheme(<Button>Click Me</Button>, { theme: 'dark' });
const button = screen.getByRole('button');
expect(button).toHaveStyle({
backgroundColor: '#333',
color: '#fff'
});
});
3. Mocking Custom Context Hooks
// Button.test.js
import { render, screen } from '@testing-library/react';
import Button from './Button';
import { useTheme } from './ThemeContext';
// Mock the custom hook
jest.mock('./ThemeContext', () => ({
useTheme: jest.fn()
}));
test('Button renders with dark theme', () => {
// Setup the mock return value
useTheme.mockReturnValue({
theme: 'dark',
toggleTheme: jest.fn()
});
render(<Button>Click Me</Button>);
const button = screen.getByRole('button');
expect(button).toHaveStyle({
backgroundColor: '#333',
color: '#fff'
});
});
Practice Activities
Activity 1: Create and Use Custom Context Hooks
Create a complete authentication system with custom hooks:
- Build an
AuthProvidercomponent that manages login/logout state - Create a custom
useAuthhook for consuming the authentication context - Implement a
useRequireAuthhook that redirects unauthenticated users - Create a
PrivateRoutecomponent that uses these hooks
Test your implementation by creating pages that require authentication and others that don't.
Activity 2: Context Selector Implementation
Implement a context selector system to improve performance:
- Create a large context with multiple values that change at different frequencies
- Implement a
useContextSelectorhook similar to the one shown earlier - Create components that only use part of the context
- Add console logs to track re-renders
- Compare performance between direct context consumption and using selectors
Activity 3: Multi-Context Integration
Build a small application that uses multiple contexts working together:
- A
UserContextfor authentication - A
ThemeContextfor UI theming - A
LocaleContextfor internationalization
Create components that consume multiple contexts and handle the interactions between them (e.g., saving theme preferences for authenticated users).
Summary
- The
useContexthook is the modern, preferred way to consume context - Custom hooks provide a clean, reusable way to consume context with added functionality
- Context selectors can help optimize performance by preventing unnecessary re-renders
- Error checking ensures context is used correctly and provides helpful developer feedback
- Advanced patterns like HOCs and dynamic context selection offer solutions for specific use cases
- Proper testing techniques ensure components work correctly with context