React Context in Action: A Hands-On Cheat-Sheet for Real-World Apps
Introduction
React’s Context API is a deceptively simple tool that can either streamline your codebase or quietly sap performance and maintainability if misused. This cheat-sheet distills the “why, when, and how” of Context into a set of actionable patterns, code snippets, and gotchas gathered from production projects — no academic fluff, just practical guidance. Inside you’ll find:
- Clear criteria for deciding whether Context, local state, or a full-blown state manager is the right fit.
- Copy-paste recipes for common use-cases like authentication, theming, feature flags, and i18n.
- Performance pitfalls you’re likely to hit (and the fixes that keep your renders snappy).
- Boilerplate you can drop into any project, plus testing tricks to keep Context-driven components rock-solid.
Use it as a quick reference while coding, a checklist during refactors, or a teaching aid for teammates who are new to Context. Master these patterns and you’ll wield React Context with confidence — no prop-drilling, no hidden re-render storms, just clean, predictable state propagation across your app.
1 · When (and when not) to use Context
Ideal use-cases
- Global, app-wide settings that change rarely e.g. current theme, locale, authenticated user
- Cross-cutting services analytics client, feature-flag SDK, i18n helpers
- Boot-time configuration API base URL, colour scheme, A/B-test branch
Situations where Context is the wrong tool
- Ephemeral UI state scoped to a couple of siblings modal open/close, tooltip visibility, form input text
- Rapid-fire data that updates every keystroke or frame mouse position, text-field value, scroll offset
- Deeply nested state you still need locally combine local state + props or a dedicated store instead
2 · Quick API reference
// 1. Create
const ThemeContext = React.createContext<"light" | "dark">("light");
// 2. Provide
function App() {
const [theme, setTheme] = useState<"light" | "dark">("light");
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Layout />
</ThemeContext.Provider>
);
}
// 3. Consume (function components)
const { theme } = useContext(ThemeContext);
// 👌 Helper hook — callers never touch raw context
export const useTheme = () => useContext(ThemeContext);
3 · Real-world recipes
Colour-mode toggle
- Read saved preference from
localStorage
. - Store it in state inside
<ThemeProvider>
. - Expose
{theme, toggle}
via context so any component can calltoggle()
.
Authenticated user
- Fetch user with SWR / React Query on app load.
- Put
{user, login, logout}
in context. - Gate routes or components with
if (!user) …
.
Feature flags
- Initialise LaunchDarkly/ConfigCat client early.
- Provide
flags
object via context. - Anywhere:
const { isEnabled } = useFlags();
.
Internationalisation
<I18nProvider locale="fr">
high in the tree.- Hook
const t = useI18n("checkout");
translates keys. - Swap locale by updating provider value.
Design-system theming
- Split concerns into multiple contexts (
Spacing
,Colours
,Typography
) to avoid full-tree renders. - Memoise each value with
useMemo
.
4 · Performance pitfalls & cures
Whole-tree re-renders
- Why: new object/array every render
- Fix:
useMemo(() => ({ theme, setTheme }), [theme])
Unrelated components updating
- Why: one all-purpose context
- Fix: create several small contexts or use context-selector libs
Infinite update loops
- Why: calling context setter during render
- Fix: move mutation into
useEffect
Stale values in async callbacks
- Why: closure captured old value
- Fix: functional updates or refs
Provider soup
- Pain: deeply nested providers hard to debug
- Fix: “ProviderComposer” wrapper or React DevTools “Context” panel
Need ultra-high-frequency reads? Reach for context selectors (
use-context-selector
), Zustand, Valtio, or Redux.
5 · Context vs. State vs. Redux — mental model
- Component state → local, transient UI concerns.
- Context → pushes global-ish data down the tree; no middleware.
- Redux/Zustand/Jotai → central store with selectors, dev-tools, time-travel; more ceremony, more power.
6 · Common anti-patterns
- Running fetch logic in the provider’s render phase. Move network calls to
useEffect
or a data-fetching hook. - Passing huge objects/functions that change every render. Stabilise with
useCallback
/useMemo
. - Treating context as an event bus. Use a tiny pub-sub library instead.
- Nesting overrides without memoisation, causing ripple renders. Override only what you need and memoise the value.
7 · Testing tips
import { render } from "@testing-library/react";
function renderWithTheme(ui, { theme = "light" } = {}) {
return render(
<ThemeContext.Provider value={{ theme, toggle: jest.fn() }}>
{ui}
</ThemeContext.Provider>
);
}
- Mount components with mock providers to test conditional rendering.
- Snapshot under multiple context combos.
8 · Copy-and-paste boilerplate
// theme-context.tsx
import { createContext, useContext, useMemo, useState } from "react";
type Theme = "light" | "dark";
interface ThemeCtx {
theme: Theme;
toggle: () => void;
}
const ThemeContext = createContext<ThemeCtx | undefined>(undefined);
export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [theme, setTheme] = useState<Theme>("light");
const value = useMemo(
() => ({
theme,
toggle: () => setTheme((t) => (t === "light" ? "dark" : "light")),
}),
[theme],
);
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
};
export const useTheme = (): ThemeCtx => {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error("useTheme must be used within <ThemeProvider>");
return ctx;
};
Conclusion
- Create context once, export a helper hook.
- Provide high in the tree; memoise the value.
- Keep context lean — only share what’s truly global.
- Split contexts or use selectors to dodge unnecessary renders.
- Test with mock providers.
Use these guidelines and React Context will stay a nimble ally rather than a hidden performance trap. Happy coding!