Part 6.2 — Context API Done Right: Providers, Consumers & Performance
Context is not a global variable system.
It’s a dependency injection mechanism.
When used deliberately, Context makes cross-cutting concerns simple and explicit. When abused, it quietly tanks performance and obscures data flow. This post shows how to use Context surgically—with clear ownership, stable values, and predictable re-render behavior.
1. When Context Is the Right Tool
Context is appropriate when state or behavior is:
needed by many distant components
conceptually global (auth, theme, locale)
relatively stable
orthogonal to UI structure
If state changes frequently or is tightly coupled to a small subtree, Context is probably the wrong choice.
2. Start with a Narrow, Purpose-Built Context
Avoid “God contexts”.
Bad:
const AppContext = createContext(null);Good:
type AuthContextValue = {
user: User | null;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
};
const AuthContext = React.createContext<AuthContextValue | undefined>(
undefined,
);Each Context should answer one question.
3. Always Wrap Context in a Provider Component
Never export raw state setters.
function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = React.useState<User | null>(null);
async function login(email: string, password: string) {
const res = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ email, password }),
});
const data = await res.json();
setUser(data.user);
}
function logout() {
setUser(null);
}
const value = { user, login, logout };
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
}The provider owns the logic. Consumers stay dumb.
4. Create a Safe Consumer Hook
Never call useContext directly throughout your app.
function useAuth() {
const ctx = React.useContext(AuthContext);
if (!ctx) {
throw new Error('useAuth must be used within AuthProvider');
}
return ctx;
}Benefits:
better error messages
single import point
easy refactoring
This pattern should be non-negotiable.
5. The Biggest Context Performance Trap
Every time the value object changes, all consumers re-render.
This line is the culprit:
const value = { user, login, logout };Because it creates a new object on every render.
6. Stabilize Context Values with useMemo
Fix it explicitly:
const value = React.useMemo(
() => ({ user, login, logout }),
[user],
);Now consumers only re-render when user actually changes.
This single change often removes the need for premature memoization elsewhere.
7. Split Contexts by Change Frequency
If part of the state changes often, split it.
Bad:
<AuthContext value={{ user, isLoading, login }} />Better:
<AuthStateContext value={user} />
<AuthActionsContext value={{ login, logout }} />Why?
userchanges occasionallylogin/logoutare stable
Consumers subscribe only to what they need.
8. Avoid Putting Server Data Directly in Context
Context is not a cache.
Avoid:
<AuthContext value={{ user, posts, comments }} />Problems:
bloated providers
frequent re-renders
unclear ownership
Server state should live closer to where it’s used (or in a dedicated server-state layer, covered later).
9. Context Is Not a Replacement for Props
Passing props is not a smell.
Prefer:
<UserAvatar user={user} />Over:
const { user } = useAuth();Props make dependencies explicit and components reusable. Context hides dependencies—use it only when that trade-off is worth it.
10. Testing Context Providers
Test providers through behavior, not internals.
it('logs user in', async () => {
render(
<AuthProvider>
<TestComponent />
</AuthProvider>,
);
await userEvent.click(screen.getByText(/login/i));
expect(screen.getByText(/welcome/i)).toBeInTheDocument();
});Never test Context by reading internal state directly.
11. Where to Place Providers in the Tree
Rules of thumb:
app-wide concerns → near root
feature-specific concerns → near feature entry
avoid wrapping the entire app unnecessarily
Providers should be as low as possible—but no lower.
12. Common Context Smells
Watch for:
providers nested 6–7 levels deep
frequent
useMemo/useCallbackband-aidscontexts holding unrelated state
“temporary” state promoted to global
These indicate boundary issues—not missing optimizations.
13. Summary
You now know how to:
design focused Contexts
build safe provider/consumer patterns
stabilize context values
avoid unnecessary re-renders
split contexts by responsibility
keep Context readable and fast
Related
Part 6.6 — Async State, Side Effects & Server State Boundaries
Most React state bugs come from mixing UI state with server state. This post teaches you how to draw a hard line between them—and why that line matters.
Part 6.5 — Introducing Redux Toolkit the Right Way
Redux isn’t the default anymore—but when your app needs it, Redux Toolkit is the cleanest, safest way to introduce global state at scale.
Part 6.4 — Combining Context + useReducer for App-Level State
Context provides access. Reducers provide structure. Together, they form a powerful, lightweight architecture for managing app-level state in React.
Comments