React State

React Context API

The Context API is React's built-in way to share state across a tree without passing props through every level. Used well, it removes prop drilling; used carelessly, it re-renders everything.

intermediate13 min readUpdated Sep 15, 2026
ThemeContext.jsx
jsx
// ThemeContext.jsx
import { createContext, useContext } from "react";

const ThemeContext = createContext("light");

export function useTheme() {
  return useContext(ThemeContext);
}

export function App() {
  return (
    <ThemeContext.Provider value="dark">
      <Toolbar />
    </ThemeContext.Provider>
  );
}
Ships with
React
Extra packages
None
Created with
createContext
Provided by
Context.Provider
Read with
useContext
Best for
Low-frequency shared values

Why it matters

Why Context exists

No prop drilling

Any component in the tree can read a shared value directly, no matter how deeply it is nested.

Zero dependencies

Context is part of React, so there is nothing to install and no extra runtime to reason about.

Composable providers

You can nest several providers and split a large value into focused contexts for better performance.

The big picture

The three parts of Context

A context object, a provider that supplies a value, and a hook that reads it. There is nothing else to learn.

The context object

Identity

Created once with createContext, it is the key components use to find a provider.

The provider

Supply

A component that puts a value into the tree and re-renders consumers when it changes.

The consumer

Read

useContext reads the nearest provider's value, and a custom hook wraps that for convenience.

Context at a glance

The core of Context

createContext

Define a context with a default value used when no provider is present.

Provider

Wrap a subtree and pass the current value to every consumer below.

useContext

Read the nearest provider value inside any component.

Custom hooks

Wrap useContext in a hook so callers get a clear API and error checks.

Splitting contexts

Separate state and actions so consumers only re-render for what they use.

Performance

Context re-renders all consumers on change, so keep values stable and focused.

A short history

From hidden API to first-class feature

  1. 2018

    Context goes stable

    React 16.3 ships a supported Context API after years of an experimental version.

    18
  2. 2019

    Hooks arrive

    useContext makes consuming context a one-liner and replaces render props.

    19
  3. 2021

    Context and performance

    The community settles on splitting contexts and memoising values as best practice.

    21
  4. Today

    A built-in tool

    Context is the default for themes, auth and locale, while stores handle high-frequency state.

    Today

The complete guide

React Context API: Everything you need to know

What is the Context API?

The Context API is React’s built-in way to pass data through a component tree without threading props through every level. It solves a specific, common problem: a value that many components need, but which sits far above them in the tree.

The classic example is theming. A Button deep in the tree needs the current theme, but every component between the top and the button would have to accept and forward a theme prop. That is prop drilling, and Context removes it. You provide the value once, and any descendant can read it directly.

Creating and providing context

Context has three pieces: an object, a provider and a consumer hook.

// AuthContext.jsx
import { createContext, useContext, useMemo, useState } from "react";

const AuthContext = createContext(null);

export function AuthProvider({ children }) {
  const [user, setUser] = useState(null);

  const value = useMemo(
    () => ({
      user,
      login: (nextUser) => setUser(nextUser),
      logout: () => setUser(null),
    }),
    [user],
  );

  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}

export function useAuth() {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error("useAuth must be used within an AuthProvider");
  }
  return context;
}

createContext defines the context and a default value. The provider supplies the current value to everything below it. The custom hook reads the value and gives a clear error if the component is used outside the provider.

Consuming context

Any descendant can read the value, no matter how deep it sits.

// Header.jsx
import { useAuth } from "./AuthContext";

export function Header() {
  const { user, logout } = useAuth();

  return (
    <header>
      <span>{user ? `Hi, ${user.name}` : "Guest"}</span>
      {user && <button onClick={logout}>Sign out</button>}
    </header>
  );
}

The Header never received a prop from App. It reached into the tree for the value it needed, which keeps intermediate components unaware of data they do not use.

Context is not state

This distinction trips people up. Context does not hold state or manage updates. It transports a value. The state itself lives in useState or useReducer, and the provider passes it down.

That is why a common pattern pairs context with a reducer:

// CartContext.jsx
import { createContext, useContext, useReducer } from "react";

const CartContext = createContext(null);

function reducer(state, action) {
  switch (action.type) {
    case "add":
      return [...state, action.item];
    case "remove":
      return state.filter((item) => item.id !== action.id);
    default:
      return state;
  }
}

export function CartProvider({ children }) {
  const [items, dispatch] = useReducer(reducer, []);
  return (
    <CartContext.Provider value={{ items, dispatch }}>
      {children}
    </CartContext.Provider>
  );
}

export const useCart = () => useContext(CartContext);

The reducer keeps updates predictable, and context makes them available anywhere without prop drilling.

Performance: the real constraint

When a provider’s value changes, every component that reads that context re-renders. There is no selector, so you cannot subscribe to only part of the value. Two habits keep this manageable.

Memoise the value. An object literal creates a new reference every render, which means every consumer re-renders even when nothing changed.

// memoised
const value = useMemo(() => ({ user, logout }), [user]);

Split contexts. Put state and actions in separate contexts, or split by feature, so a component that only needs to dispatch an action does not re-render when the state changes.

// split.jsx
const CartStateContext = createContext(null);
const CartActionsContext = createContext(null);

Components that only call actions subscribe to the actions context, which never changes identity, so they never re-render from a state update. This one trick solves most context performance complaints.

When to use Context, and when not to

Context is the right tool for:

  • Theme and colour mode.
  • The authenticated user and session.
  • Locale and translations.
  • A router or a small, stable configuration.

Reach for a dedicated store when you have:

  • Frequent updates, such as typing, dragging or animations.
  • A large state tree where components need selectors.
  • A need for middleware, persistence or time-travel devtools.
  • Server state that needs caching and refetching.

Libraries like Zustand and Redux Toolkit add selectors so components subscribe to slices rather than the whole value. For server data, TanStack Query handles caching entirely.

Best practices

  • Wrap useContext in a custom hook and check for a missing provider.
  • Memoise provider values with useMemo.
  • Split contexts by concern, especially state versus actions.
  • Keep context values small and focused.
  • Use useReducer when updates are complex or depend on previous state.
  • Do not use context for high-frequency updates; use a selector-based store.
  • Provide sensible defaults so components can render outside a provider when appropriate.

Common mistakes

  • Treating context as a state manager and expecting selectors.
  • Passing an inline object as the provider value and re-rendering everything.
  • Putting unrelated data in one giant context.
  • Forgetting the default value and crashing on undefined.
  • Using context for values that only one or two components need.
  • Expecting context to optimise re-renders automatically.

Where to go next

Context is the foundation of React state sharing, and knowing its limits tells you when to graduate. Compare it with Redux Toolkit for predictable large-scale state and Zustand for a lightweight selector store, then handle server data with TanStack Query. For Vue, the equivalent concept is covered in the Pinia guide.

Sharing a value

Context is for values many components need, such as theme or the current user. It is not a replacement for every prop.

Prefer
const ThemeContext = createContext("light");

function Button() {
  const theme = useContext(ThemeContext);
  return <button className={theme} />;
}
Avoid
function Button({ theme }) {
  return <button className={theme} />;
}
// drilled through every
// level of the tree

Providing a value

An inline object creates a new reference every render, re-rendering every consumer. Memoise it.

Prefer
const value = useMemo(
  () => ({ user, logout }),
  [user],
);

<AuthContext.Provider value={value}>
  {children}
</AuthContext.Provider>
Avoid
<AuthContext.Provider
  value={{ user, logout }}
>
  {children}
</AuthContext.Provider>

FAQ

Frequently asked questions

Keep learning

Related topics from the roadmap.

$ start learning

Ready to start learning Context API?

Our interactive tutorial walks you through Context API step by step — with quizzes and real code you can run in the browser.