useContext
Passing data through many layers of components by hand is tedious and brittle — every intermediate component has to forward props it never uses. The Context API solves this “prop drilling” problem by letting a parent broadcast a value to any descendant, no matter how deep. The useContext hook is how a function component subscribes to that value. It reads from the nearest matching Provider above it in the tree and re-renders whenever that value changes.
Creating a context
A context is created once, usually in its own module, with createContext. The argument you pass becomes the default value — used only when a component consumes the context with no matching Provider above it.
// ThemeContext.js
import { createContext } from "react";
export const ThemeContext = createContext("light");
createContext returns an object that holds two important pieces: ThemeContext.Provider, which supplies a value, and the context object itself, which you hand to useContext to read that value. You do not call the context like a function — you pass it around as a reference.
Providing a value
Wrap part of your tree in the Provider and pass a value prop. Every descendant — at any depth — can now read it.
import { useState } from "react";
import { ThemeContext } from "./ThemeContext";
import Toolbar from "./Toolbar";
export default function App() {
const [theme, setTheme] = useState("dark");
return (
<ThemeContext.Provider value={theme}>
<Toolbar />
<button onClick={() => setTheme((t) => (t === "dark" ? "light" : "dark"))}>
Toggle theme
</button>
</ThemeContext.Provider>
);
}
Note that Toolbar receives no theme prop. It can sit several components deep and still reach the value.
Consuming with useContext
Call useContext with the context object. It returns the current value from the nearest Provider above the calling component.
import { useContext } from "react";
import { ThemeContext } from "./ThemeContext";
function ThemedButton() {
const theme = useContext(ThemeContext);
return (
<button className={`btn btn-${theme}`}>
Current theme: {theme}
</button>
);
}
export default function Toolbar() {
return (
<div>
<ThemedButton />
</div>
);
}
Output:
Renders a button reading: "Current theme: dark"
Like every hook, useContext must be called at the top level of a component or a custom hook — never inside loops, conditions, or nested functions.
Default values
The default passed to createContext is a fallback, not a starting state. It applies only when there is no Provider in the tree.
import { createContext, useContext } from "react";
const UserContext = createContext({ name: "Guest", role: "anon" });
function Profile() {
const user = useContext(UserContext);
return <p>{user.name} ({user.role})</p>;
}
// Without a Provider, Profile renders "Guest (anon)"
Tip: A sensible default makes a component usable on its own and lets you write tests without wrapping in a Provider. Use it to encode the “no Provider” scenario explicitly.
Re-render behavior
This is the part that catches people out. When the Provider’s value changes, every component that consumes that context re-renders — even ones whose visible output does not depend on the part that changed. React compares the new value to the old with Object.is. If you pass a fresh object or function literal on each render, the comparison always fails and consumers re-render every time.
// Problem: a new object every render -> all consumers re-render constantly
<UserContext.Provider value={{ name, role }}>
// Fix: memoize the value so its identity is stable
const value = useMemo(() => ({ name, role }), [name, role]);
<UserContext.Provider value={value}>
| Concern | What happens | What to do |
|---|---|---|
| Provider value is a new object each render | All consumers re-render | Wrap the value in useMemo |
| Many unrelated values in one context | Unrelated consumers re-render | Split into multiple contexts |
| Frequently changing value | Wide re-render fan-out | Consider an external store |
Warning:
useContextdoes not let you subscribe to part of a value. If a consumer only needsnamebut the context also holdsrole, it still re-renders whenrolechanges. Splitting state into focused contexts is the simplest mitigation.
A typed context (TypeScript)
In TypeScript, model the shape and provide a custom hook that guards against a missing Provider.
import { createContext, useContext, useState, type ReactNode } from "react";
interface AuthState {
user: string | null;
login: (name: string) => void;
}
const AuthContext = createContext<AuthState | null>(null);
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<string | null>(null);
const value: AuthState = { user, login: setUser };
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
export function useAuth(): AuthState {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error("useAuth must be used within an AuthProvider");
return ctx;
}
This pattern hides the raw context, gives callers a clean useAuth() API, and fails loudly when the Provider is forgotten.
Best practices
- Define each context in its own module and export both the Provider wrapper and a custom consumer hook.
- Memoize the Provider
valuewithuseMemo(and callbacks withuseCallback) so consumers don’t re-render needlessly. - Split large contexts into smaller, focused ones to keep re-renders narrow.
- Provide a meaningful default, or guard for a missing Provider with a clear error.
- Reach for context for genuinely shared, slowly changing data (theme, locale, auth) — not for high-frequency state where an external store fits better.
- Keep business logic in a Provider component; let consumers stay declarative.