Skip to content
React rc state-management 4 min read

Context API

React Context lets you broadcast a value to an entire component subtree without passing props through every intermediate level. It is the built-in cure for prop drilling—threading the same prop through layers of components that never read it. Context shines for stable, cross-cutting concerns like the current theme, the signed-in user, or the active locale, and it ships with React, so there is nothing to install.

Creating a context

createContext makes a context object once, at module scope. The argument is the default value used only when a component reads the context with no matching provider above it. Treat that default as a fallback for tests and standalone use, not as the real value.

import { createContext } from "react";

export const ThemeContext = createContext("light");

The returned object exposes a Provider component and is what you pass to useContext. Define it in its own module so both providers and consumers can import the same reference.

Providing a value

Wrap a subtree in <Context.Provider value={...}> to supply the value every descendant will read. The value prop is required; whatever you put there becomes available to all consumers below, no matter how deep.

import { useState } from "react";
import { ThemeContext } from "./ThemeContext";

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

A provider can appear anywhere in the tree, and you can even nest the same provider to override the value for a branch. The nearest provider wins.

Reading with useContext

Any descendant reads the current value with the useContext hook. There is no subscription boilerplate—the component simply re-renders whenever the provider’s value changes.

import { useContext } from "react";
import { ThemeContext } from "./ThemeContext";

function Toolbar() {
  const theme = useContext(ThemeContext);
  return <div className={`bar bar-${theme}`}>Active theme: {theme}</div>;
}

Output:

Active theme: dark

Note: in React 19 you can also render <ThemeContext> directly as the provider (without .Provider). The .Provider form still works and remains the safest choice for code that targets multiple React versions.

A typed AuthContext

A common real-world pattern bundles a context together with a provider component and a custom hook. This keeps the shape in one place, gives consumers a clean API, and—in TypeScript—lets the hook guarantee the value is present.

import { createContext, useContext, useMemo, useState, type ReactNode } from "react";

interface User {
  id: string;
  name: string;
}

interface AuthValue {
  user: User | null;
  login: (name: string) => void;
  logout: () => void;
}

const AuthContext = createContext<AuthValue | null>(null);

export function AuthProvider({ children }: { children: ReactNode }) {
  const [user, setUser] = useState<User | null>(null);

  const value = useMemo<AuthValue>(
    () => ({
      user,
      login: (name) => setUser({ id: crypto.randomUUID(), name }),
      logout: () => setUser(null),
    }),
    [user]
  );

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

export function useAuth(): AuthValue {
  const ctx = useContext(AuthContext);
  if (!ctx) throw new Error("useAuth must be used inside <AuthProvider>");
  return ctx;
}

Consumers never touch useContext directly—they call useAuth, which throws a clear error if the provider is missing instead of silently handing back null.

import { useAuth } from "./AuthContext";

function Header() {
  const { user, login, logout } = useAuth();
  return user ? (
    <button onClick={logout}>Sign out {user.name}</button>
  ) : (
    <button onClick={() => login("Ada")}>Sign in</button>
  );
}

Output:

(initially)  Sign in
(after click)  Sign out Ada

Wrapping the value in useMemo matters: without it, the provider hands consumers a brand-new object on every render, forcing them all to re-render even when nothing changed.

Composing multiple contexts

Apps usually have several cross-cutting concerns at once. Each gets its own context and provider, and you compose them by nesting. Keeping them separate means a theme change does not re-render auth consumers and vice versa.

function Providers({ children }) {
  return (
    <AuthProvider>
      <ThemeProvider>
        <LocaleProvider>{children}</LocaleProvider>
      </ThemeProvider>
    </AuthProvider>
  );
}

When the nesting gets deep, a small helper can flatten it, but separate providers per concern remain the right design—do not merge unrelated values into one mega-context.

The limits of Context

Context is a transport, not a state manager. It moves a value down the tree; it does not store, update, batch, or optimize anything for you. Understanding what it does not do prevents misuse.

ConcernContextA real store (Redux/Zustand)
Pass value deepYesYes
Holds its own stateNo (you pair it with useState/useReducer)Yes
Selective subscriptionsNo—every consumer re-renders on any changeYes, via selectors
Async/middlewareNoYes
DevTools/time travelNoYes (Redux)

The key limitation is re-rendering: every consumer re-renders whenever the provider value changes, with no way to subscribe to just one field. For frequently changing data with many readers, that becomes a performance problem—see Context performance for splitting contexts and other mitigations.

Best Practices

  • Use Context for stable, low-frequency values: theme, auth, locale, feature flags.
  • Pair Context with useState or useReducer—Context alone holds no state.
  • Memoize object/array values with useMemo so consumers do not re-render needlessly.
  • Ship a custom hook (useAuth) that throws when the provider is missing.
  • Split unrelated concerns into separate contexts rather than one big value.
  • Do not reach for Context to avoid passing one prop one level—lift state instead.
  • Promote to a real store when you need selectors, middleware, or rapid updates.
Last updated June 14, 2026
Was this helpful?