Design Patterns

Context API React Design Pattern

I had a theme toggle that needed to reach 15 components deep. Passing theme and toggleTheme as props through every intermediate component — components tha...

14 Apr 2024

Context API React Design Pattern

I had a theme toggle that needed to reach 15 components deep. Passing theme and toggleTheme as props through every intermediate component — components that didn't even use the theme — was painful. The code was noisy and every refactor risked breaking the prop chain.

The Context API solves prop drilling. It lets you share data across the component tree without threading props through every level. You create a context, wrap a subtree with a Provider, and any descendant can consume the value directly.

Think of it like a building's PA system. Instead of passing a message person-to-person through every floor, you broadcast it. Anyone who cares listens.

Jsx
import { createContext, useState, useContext } from 'react';

const ThemeContext = createContext();

export const ThemeProvider = ({ children }) => {
  const [theme, setTheme] = useState('light');

  const toggleTheme = () => {
    setTheme(prev => (prev === 'light' ? 'dark' : 'light'));
  };

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
};

export const useTheme = () => useContext(ThemeContext);

Wrap your app with the provider:

Jsx
import { ThemeProvider } from './ThemeContext';
import Header from './Header';
import Content from './Content';

const App = () => (
  <ThemeProvider>
    <Header />
    <Content />
  </ThemeProvider>
);

export default App;

Now any component inside ThemeProvider can call useTheme() and get theme and toggleTheme — no matter how deeply nested it is.

When It Fits

  • Themes, locale, auth. Data that many components need but few components change.
  • Avoiding prop drilling. When you're passing props through 3+ levels of components that don't use them.
  • Simple shared state. A few values that update infrequently.

When It Doesn't

Context re-renders every consumer when the value changes. If you're storing fast-changing state (like form input values or mouse position), every consumer re-renders on every change. That kills performance.

For complex state management with frequent updates, reach for Zustand, Jotai, or Redux. Context is not a state management library — it's a dependency injection mechanism.

The benefit: Zero prop drilling. Clean API. Built into React — no extra dependencies. Perfect for low-frequency global values.

The cost: No built-in performance optimization. Every context change re-renders all consumers. Nesting multiple providers can create "provider hell" in your app root. And it's easy to overuse — not everything belongs in context.

I use Context for themes, auth state, and locale. For anything more complex, I pair it with useReducer or reach for a dedicated state library.

Keep reading