Advanced30 min read

Context API

Master React's Context API to share data across your component tree without prop drilling. Learn createContext, Provider, useContext, and when to use context for themes, auth, and locale.

The Prop Drilling Problem

In React, data flows downward from parent components to child components via props. This is a great design for simple component trees. But what happens when a piece of data needs to be accessed by a deeply nested component?

Imagine a theme value that is set at the top level but needed by a button five levels deep:

jsx
function App() {
  const theme = 'dark';
  return <Layout theme={theme} />;
}

function Layout({ theme }) {
  return <Sidebar theme={theme} />;
}

function Sidebar({ theme }) {
  return <Menu theme={theme} />;
}

function Menu({ theme }) {
  return <MenuItem theme={theme} />;
}

function MenuItem({ theme }) {
  return <button className={theme}>Click me</button>;
}

Every intermediate component (Layout, Sidebar, Menu) receives theme only to pass it further down. They do not use it themselves. This pattern is called prop drilling, and it creates several problems:

  • Verbose code: Every component in the chain must declare and forward the prop.
  • Tight coupling: Intermediate components become dependent on props they do not use.
  • Refactoring pain: Adding or renaming a shared prop means updating every component in the chain.
  • Scalability issues: As your app grows, you end up drilling many props through many levels.

React's Context API solves this by allowing you to share values across the component tree without passing props through every level.

createContext and Provider

The Context API has three steps: create a context, provide a value, and consume the value.

Step 1: Create a context

jsx
const ThemeContext = React.createContext('light');

createContext takes a default value as its argument. This default is used only when a component reads the context but there is no matching Provider above it in the tree. Think of it as a fallback.

Step 2: Provide a value

Wrap a portion of your component tree with the context's Provider component and pass it a value prop:

jsx
function App() {
  return (
    <ThemeContext.Provider value="dark">
      <Layout />
    </ThemeContext.Provider>
  );
}

Every component inside <ThemeContext.Provider> — no matter how deeply nested — can access the value "dark". The value prop on the Provider overrides the default value passed to createContext.

Dynamic values

The Provider value can be state, making the context reactive:

jsx
function App() {
  const [theme, setTheme] = React.useState('light');

  return (
    <ThemeContext.Provider value={theme}>
      <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
        Toggle Theme
      </button>
      <Layout />
    </ThemeContext.Provider>
  );
}

When theme changes, React re-renders all components that consume ThemeContext.

useContext: Consuming Context Values

Step 3: Consume the value with useContext

The useContext hook reads the current value from the nearest Provider above the component in the tree:

jsx
function ThemedButton() {
  const theme = React.useContext(ThemeContext);
  return <button className={theme}>I am {theme}!</button>;
}

No matter how deep ThemedButton is in the component tree, it can read the theme directly — no prop drilling required.

How React finds the value

When a component calls useContext(ThemeContext), React walks up the component tree looking for the nearest <ThemeContext.Provider>. It uses that Provider's value. If no Provider is found, it uses the default value from createContext.

jsx
// Outer Provider: theme = 'dark'
<ThemeContext.Provider value="dark">
  <Navbar />  {/* reads 'dark' */}

  {/* Inner Provider: theme = 'light' */}
  <ThemeContext.Provider value="light">
    <Sidebar />  {/* reads 'light' — nearest Provider wins */}
  </ThemeContext.Provider>
</ThemeContext.Provider>

A common pattern: Context + Custom Hook

It is common to wrap useContext in a custom hook for better ergonomics and error handling:

jsx
const ThemeContext = React.createContext(undefined);

function ThemeProvider({ children }) {
  const [theme, setTheme] = React.useState('light');
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

function useTheme() {
  const context = React.useContext(ThemeContext);
  if (context === undefined) {
    throw new Error('useTheme must be used within a ThemeProvider');
  }
  return context;
}

// Usage in any component:
function Header() {
  const { theme, setTheme } = useTheme();
  return <h1 className={theme}>My App</h1>;
}

This pattern gives you type safety and a helpful error message if a component tries to use the context outside its Provider.

When to Use Context

Context is ideal for data that is global or semi-global within a part of your app. Common use cases:

  • Theme (light/dark mode): Nearly every component may need to know the current theme.
  • Authentication: The current user object and login/logout functions.
  • Locale / i18n: The current language and translation function.
  • Feature flags: Which features are enabled for the current user.
  • UI state: Sidebar open/closed, toast notifications.

When NOT to use Context

Context is not a replacement for all props. Avoid it when:

  • Data is only needed by one or two levels. Just pass props. Context adds complexity.
  • Data changes very frequently (e.g., mouse position at 60fps). Every context update re-renders all consumers. For high-frequency updates, consider a ref or a state management library.
  • You want fine-grained control. Context does not have selectors — when the context value changes, all consumers re-render, even if they only use a part of the value. Libraries like Zustand or Jotai offer subscriptions to specific pieces of state.

Multiple Contexts

It is perfectly fine (and recommended) to split your context by domain rather than having one giant context:

jsx
function App() {
  return (
    <AuthProvider>
      <ThemeProvider>
        <LocaleProvider>
          <Layout />
        </LocaleProvider>
      </ThemeProvider>
    </AuthProvider>
  );
}

This keeps contexts focused and prevents unnecessary re-renders. When the theme changes, only components that consume ThemeContext re-render — components that only consume AuthContext are unaffected.

Context API in Action

html
<div id="root"></div>
<script src="https://unpkg.com/react@19/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@19/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
  const ThemeContext = React.createContext("light");

  function ThemeProvider({ children }) {
    const [theme, setTheme] = React.useState("light");
    const toggle = () => setTheme(t => t === "light" ? "dark" : "light");
    return (
      <ThemeContext.Provider value={{ theme, toggle }}>
        {children}
      </ThemeContext.Provider>
    );
  }

  function ThemedBox() {
    const { theme, toggle } = React.useContext(ThemeContext);
    const style = {
      background: theme === "dark" ? "#333" : "#eee",
      color: theme === "dark" ? "#fff" : "#000",
      padding: "20px",
      borderRadius: "8px",
    };
    return (
      <div style={style}>
        <p>Current theme: {theme}</p>
        <button onClick={toggle}>Toggle Theme</button>
      </div>
    );
  }

  function App() {
    return (
      <ThemeProvider>
        <h2>Context API Demo</h2>
        <ThemedBox />
      </ThemeProvider>
    );
  }

  ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>

What happens when a component calls useContext but there is no matching Provider above it in the tree?

Ready to practice?

Create your free account to access the interactive code editor, run challenges, and track your progress.