Context solves prop drilling, the tedium of passing a value down through several layers of components that don't actually use it themselves, just to reach a deeply nested component that does — a Context.Provider higher up the tree supplies a value, and any descendant component can read it directly with useContext(MyContext), regardless of how many intermediate components sit in between, without those intermediate components needing to know or forward that value at all. A component using useContext automatically re-renders whenever the Provider's value changes, making context suitable for genuinely shared, cross-cutting data like a theme, authenticated user, or locale setting.
1Understanding useContext
Context solves prop drilling, the tedium of passing a value down through several layers of components that don't actually use it themselves, just to reach a deeply nested component that does — a Context.Provider higher up the tree supplies a value, and any descendant component can read it directly with useContext(MyContext), regardless of how many intermediate components sit in between, without those intermediate components needing to know or forward that value at all. A component using useContext automatically re-renders whenever the Provider's value changes, making context suitable for genuinely shared, cross-cutting data like a theme, authenticated user, or locale setting.
Every component consuming a context with useContext re-renders whenever that context's value changes, even if the component only cares about part of a large value object — for state that changes frequently, consider splitting it into more granular contexts to avoid triggering unnecessary re-renders across unrelated consumers.
import { createContext, useContext } from 'react';
const ThemeContext = createContext('light');
function ThemedButton() {
const theme = useContext(ThemeContext);
return <button className={theme}>Click</button>;
}
function App() {
return (
<ThemeContext.Provider value="dark">
<ThemedButton />
</ThemeContext.Provider>
);
}2Practical Example
Here is a real-world application of useContext showing how it is used in production React code.
import { createContext, useContext, useState } from 'react';
const UserContext = createContext(null);
function Profile() {
const user = useContext(UserContext);
return <p>Logged in as: {user?.name ?? 'Guest'}</p>;
}
function App() {
const [user] = useState({ name: 'Ana' });
return (
<UserContext.Provider value={user}>
<Profile />
</UserContext.Provider>
);
}3Best Practices
Follow these guidelines when working with useContext:
1. Use context for genuinely global or cross-cutting values, like theme, authenticated user, or locale, not as a blanket replacement for regular props between closely related components
2. Split frequently-changing state into its own, more granular context rather than bundling it with rarely-changing values, to avoid triggering unnecessary re-renders in components that only need the stable part
3. Provide a sensible default value when creating the context with createContext(), so components still behave reasonably even if used outside of an explicit Provider
Tip: Every component consuming a context with useContext re-renders whenever that context's value changes, even if the component only cares about part of a large value object — for state that changes frequently, consider splitting it into more granular contexts to avoid triggering unnecessary re-renders across unrelated consumers.
import { createContext, useContext } from 'react';
const ThemeContext = createContext('light');
function ThemedButton() {
const theme = useContext(ThemeContext);
return <button className={theme}>Click</button>;
}
function App() {
return (
<ThemeContext.Provider value="dark">
<ThemedButton />
</ThemeContext.Provider>
);
}4Memoize the Provider's Value
An object literal passed as a Provider's value prop is a brand-new reference every time the Provider's parent re-renders, so every consumer of that context re-renders too — even when none of the actual data changed. Wrapping the value in useMemo keeps the same reference across renders unless its own dependencies change, preventing that re-render storm.
// New object every render -> all consumers re-render
<Ctx.Provider value={{ user, theme }}>
// Stable reference -> only re-renders when user/theme change
const value = useMemo(() => ({ user, theme }), [user, theme]);
<Ctx.Provider value={value}>5Nested Providers: The Nearest One Wins
A consumer always reads from the closest matching Provider above it in the tree, not the outermost one. This lets an inner subtree locally override a context's value — like a dark-mode panel nested inside an otherwise light-themed app — without affecting any consumer outside that subtree.
<ThemeContext.Provider value="light">
<Sidebar /> {/* sees 'light' */}
<ThemeContext.Provider value="dark">
<Panel /> {/* sees 'dark' */}
</ThemeContext.Provider>
</ThemeContext.Provider>