Consuming Context with useContext

Accessing and Using Context Values in React Components

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:

flowchart TD A[Ways to Consume Context] A --> B[useContext Hook] A --> C[Context.Consumer] A --> D[Static contextType] A --> E[Custom Hooks] style B fill:#dfd,stroke:#3a3 style C fill:#ffd,stroke:#aa3 style D fill:#fdd,stroke:#a33 style E fill:#ddf,stroke:#33a B --- BN["Functional Components (Recommended)"] C --- CN["JSX-based (Legacy)"] D --- DN["Class Components Only (Legacy)"] E --- EN["Encapsulated Logic + useContext"]

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:

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:


// 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 cart
  • useAuth() - For handling user authentication
  • useWishlist() - 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:

  1. Build an AuthProvider component that manages login/logout state
  2. Create a custom useAuth hook for consuming the authentication context
  3. Implement a useRequireAuth hook that redirects unauthenticated users
  4. Create a PrivateRoute component 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:

  1. Create a large context with multiple values that change at different frequencies
  2. Implement a useContextSelector hook similar to the one shown earlier
  3. Create components that only use part of the context
  4. Add console logs to track re-renders
  5. Compare performance between direct context consumption and using selectors

Activity 3: Multi-Context Integration

Build a small application that uses multiple contexts working together:

  1. A UserContext for authentication
  2. A ThemeContext for UI theming
  3. A LocaleContext for internationalization

Create components that consume multiple contexts and handle the interactions between them (e.g., saving theme preferences for authenticated users).

Summary

Further Resources