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.Providerform 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.
| Concern | Context | A real store (Redux/Zustand) |
|---|---|---|
| Pass value deep | Yes | Yes |
| Holds its own state | No (you pair it with useState/useReducer) | Yes |
| Selective subscriptions | No—every consumer re-renders on any change | Yes, via selectors |
| Async/middleware | No | Yes |
| DevTools/time travel | No | Yes (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
useStateoruseReducer—Context alone holds no state. - Memoize object/array values with
useMemoso 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.