useContext Hook
The useContext hook lets a function component read a value from a React Context without passing that value down through props at every level. It solves “prop drilling” — the pain of threading a prop through five components that don’t care about it, just so the sixth one can use it. Instead, a component higher in the tree provides a value, and any descendant can pull it out directly with useContext.
Overview / How it works
Context is React’s built-in mechanism for sharing data that many components need, such as the current theme, the logged-in user, or the active language. Without context, you’d pass that data as props through every intermediate component, even ones that only forward it along. Context skips the middlemen.
Using context always has two halves. First, you create a context object with createContext, which returns a Provider component (and, historically, a Consumer component — but with hooks you rarely touch Consumer directly). Second, any component below that provider in the tree calls useContext(MyContext) to read the current value. React doesn’t search props or state trees to resolve this — it walks up the component tree from the calling component looking for the nearest matching Provider, and reads the value that provider was given.
Under the hood, when the value passed to a Provider changes (compared by reference, using Object.is), React re-renders every component that calls useContext for that context, regardless of whether that component also re-renders for other reasons. This is a crucial fact: context updates are not selective by field — if your context value is an object { user, theme } and only theme changes, every consumer re-renders, even ones that only read user. This is why context works best for data that changes infrequently (theme, locale, authenticated user) rather than something that changes on every keystroke.
Because useContext is a hook, it follows the Rules of Hooks: it must be called unconditionally at the top level of a function component or custom hook, never inside a loop, condition, or nested function. If no matching Provider exists above the calling component, useContext simply returns the default value you passed to createContext — it does not throw an error, which is a common source of silent bugs.
Syntax
const MyContext = createContext(defaultValue);
function Provider({ children }) {
return (
<MyContext.Provider value={someValue}>
{children}
</MyContext.Provider>
);
}
function Consumer() {
const value = useContext(MyContext);
// use value
}
| Part | Meaning |
|---|---|
createContext(defaultValue) |
Creates a context object. defaultValue is used only when a component calls useContext with no matching Provider above it. |
MyContext.Provider |
A component that supplies a value to all descendants. Wrap the part of the tree that needs access to the value. |
value prop |
The data the provider exposes. Every consumer re-renders when this changes (by reference). |
useContext(MyContext) |
Reads the nearest enclosing provider’s value. Must be called at the top level of a component or custom hook. |
Examples
Example 1: A theme context
import { createContext, useContext, useState } from "react";
const ThemeContext = createContext("light");
function App() {
const [theme, setTheme] = useState("light");
return (
<ThemeContext.Provider value={theme}>
<button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
Toggle theme
</button>
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar() {
// No props needed — ThemedButton reads the theme itself.
return (
<div>
<ThemedButton />
</div>
);
}
function ThemedButton() {
const theme = useContext(ThemeContext);
return (
<button
style={{
background: theme === "dark" ? "#222" : "#eee",
color: theme === "dark" ? "#fff" : "#000",
}}
>
Current theme: {theme}
</button>
);
}
export default App;
Renders: a toggle button and a themed button whose background flips between light and dark. Notice Toolbar never receives or forwards a theme prop — it’s just a pass-through component. ThemedButton, two levels deep, reads theme directly from context. Clicking “Toggle theme” updates state in App, which changes the value passed to ThemeContext.Provider, which re-renders every consumer, including ThemedButton.
Example 2: Sharing an object — auth context
import { createContext, useContext, useState, useMemo } from "react";
const AuthContext = createContext(null);
function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const login = (name) => setUser({ name });
const logout = () => setUser(null);
const value = useMemo(() => ({ user, login, logout }), [user]);
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
function useAuth() {
const context = useContext(AuthContext);
if (context === null) {
throw new Error("useAuth must be used within an AuthProvider");
}
return context;
}
function Nav() {
const { user, logout } = useAuth();
return (
<nav>
{user ? (
<button onClick={logout}>Log out {user.name}</button>
) : (
<span>Not logged in</span>
)}
</nav>
);
}
function LoginForm() {
const { login } = useAuth();
return <button onClick={() => login("Ada")}>Log in as Ada</button>;
}
export default function App() {
return (
<AuthProvider>
<Nav />
<LoginForm />
</AuthProvider>
);
}
Renders: a nav bar showing “Not logged in” and a “Log in as Ada” button. Clicking it calls login, which sets user to { name: "Ada" }; the nav then shows “Log out Ada”. This example demonstrates two important patterns: bundling both data (user) and behavior (login, logout) into one context value, and wrapping useContext in a custom hook (useAuth) that throws a helpful error if used outside its provider, instead of silently returning null.
Example 3: Avoiding unnecessary re-renders with useMemo
// Inside AuthProvider from Example 2, this line matters a lot:
const value = useMemo(() => ({ user, login, logout }), [user]);
// Without useMemo, every render of AuthProvider creates a brand-new
// object literal, even if `user` hasn't changed:
const value = { user, login, logout }; // new reference every render
This fragment isolates the fix from Example 2. Because object and function literals are recreated on every render, passing { user, login, logout } directly as the value prop gives every consumer a new reference on every render of AuthProvider — triggering re-renders in Nav and LoginForm even when nothing meaningful changed. Wrapping it in useMemo with [user] as the dependency ensures the object reference only changes when user actually changes.
How it works step by step
On mount: React renders the provider, evaluates its value prop, and stores it. Each descendant that calls useContext subscribes to that context and reads the current value during its own render.
On a state update in the provider: if the new value is a different reference than the last one (checked with Object.is, the same comparison used for state updates), React schedules a re-render for every subscribed consumer — even ones nested deep in the tree and even if they’re wrapped in memo, because context reads bypass memo‘s prop comparison.
On unmount: consumers simply stop rendering; there’s no special cleanup tied to useContext itself (unlike useEffect, which needs cleanup functions for subscriptions).
Common Mistakes
Mistake 1: Calling a hook conditionally.
function Badge({ show }) {
if (!show) return null;
const theme = useContext(ThemeContext); // called conditionally — breaks hook order
return <span>{theme}</span>;
}
Hooks must run in the exact same order on every render. Putting useContext after an early return means it sometimes runs and sometimes doesn’t, which corrupts React’s internal hook bookkeeping. Fix: call the hook unconditionally at the top, then branch afterward.
function Badge({ show }) {
const theme = useContext(ThemeContext);
if (!show) return null;
return <span>{theme}</span>;
}
Mistake 2: Forgetting the provider and trusting the default value.
const CartContext = createContext(); // no default value — becomes undefined
function CartTotal() {
const { items } = useContext(CartContext); // crashes: undefined has no `items`
return <p>Total items: {items.length}</p>;
}
If <CartTotal /> is ever rendered outside a <CartContext.Provider>, useContext returns undefined (the implicit default), and destructuring it throws. Fix: give createContext a sensible default, or better, wrap the read in a custom hook that throws a clear error, as shown in Example 2’s useAuth.
Best Practices
- Use context for data that’s genuinely global to a subtree — theme, auth, locale, routing — not for passing data between two or three closely related components, where props are simpler and easier to trace.
- Wrap
useContextcalls in a custom hook (likeuseAuthoruseTheme) so consumers get a clean API and a helpful error if the provider is missing. - Memoize object/array/function values passed to a provider with
useMemo/useCallbackto avoid re-rendering every consumer on every provider render. - Split contexts that change at different rates (e.g. a rarely-changing
ThemeContextand a frequently-changingCartContext) into separate providers so unrelated consumers aren’t re-rendered together. - Keep provider components close to where the data is actually needed rather than always at the app root — this limits the re-render blast radius.
- For state that’s more complex than a single value, pair context with
useReducerinstead of severaluseStatecalls, and provide both state anddispatchthrough context.
Practice Exercises
- Create a
LanguageContextthat stores a language code ("en"or"es") and asetLanguagefunction. Build aGreetingcomponent two levels deep that reads the context and renders “Hello” or “Hola” accordingly. - Take the
AuthProviderfrom Example 2 and add auseReducer-based version that supportsLOGINandLOGOUTactions instead of two separate functions. Provide bothstateanddispatchthrough the context value. - Deliberately remove the
useMemowrapper from a context provider’s value, add a console log inside a consumer component’s body, and reason about (or verify) how many times the consumer logs when an unrelated sibling state updates in the provider.
Summary
useContextreads the nearestProvider‘s value, letting descendants skip prop drilling.- Context updates re-render every subscribed consumer when the provider’s value reference changes, regardless of which fields actually changed.
useContextfollows the Rules of Hooks: top level only, never inside conditions or loops.- With no matching provider,
useContextsilently returns the context’s default value rather than throwing. - Memoize provider values with
useMemo/useCallbackto avoid unnecessary re-renders of every consumer. - Wrapping context reads in a custom hook (
useAuth,useTheme) gives cleaner call sites and clearer errors when a provider is missing.
