Context API
React Context lets any descendant read a value without prop-drilling. Pair createContext + Provider with useContext for theme, auth, locale, feature flags — values that many components need but rarely change.
createContext, Provider, performance
EXAMPLE
// 1) Create + provide
import { createContext, useContext, useState, useMemo } from 'react';
const ThemeContext = createContext({ theme: 'light', toggle: () => {} });
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const value = useMemo(
() => ({ theme, toggle: () => setTheme((t) => (t === 'light' ? 'dark' : 'light')) }),
[theme],
);
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
}
// 2) Consume
function Button() {
const { theme, toggle } = useContext(ThemeContext);
return <button onClick={toggle}>Theme: {theme}</button>;
}
export default function App() {
return <ThemeProvider><Button /></ThemeProvider>;
}
// 3) Custom hook — cleaner API + runtime guard
function useTheme() {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error('useTheme must be used inside ThemeProvider');
return ctx;
}
// 4) Real-world: auth context with async state
const AuthContext = createContext(null);
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
api.me().then(setUser).finally(() => setLoading(false));
}, []);
const value = useMemo(() => ({
user, loading,
signIn: async (email, pw) => setUser(await api.signIn(email, pw)),
signOut: async () => { await api.signOut(); setUser(null); },
}), [user, loading]);
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
export const useAuth = () => useContext(AuthContext);
// 5) Splitting state vs dispatch — perf trick
const StateContext = createContext();
const DispatchContext = createContext();
function CounterProvider({ children }) {
const [state, dispatch] = useReducer(reducer, initial);
return (
<StateContext.Provider value={state}>
<DispatchContext.Provider value={dispatch}>{children}</DispatchContext.Provider>
</StateContext.Provider>
);
}
// Components that only DISPATCH don't re-render when state changes.
// 6) Composing providers
<AuthProvider>
<ThemeProvider>
<I18nProvider>
<App />
</I18nProvider>
</ThemeProvider>
</AuthProvider>
// Wrap with a helper to flatten:
function compose(...providers) {
return ({ children }) => providers.reduceRight((acc, P) => <P>{acc}</P>, children);
}
// 7) Default values matter
const LocaleContext = createContext('en-AU');
// If no Provider above, consumers get 'en-AU'. Useful for tests + Storybook.
// 8) Performance considerations
// • Every consumer re-renders when value changes (any field)
// • Mitigations:
// - useMemo the value object
// - Split into smaller contexts (auth vs profile vs settings)
// - Use Zustand / Jotai / Redux for hot paths
// - use-context-selector library for true selector-based subscriptions
// Bad: new object every render → every consumer re-renders
<MyContext.Provider value={{ a, b, c }}> // BAD
// Good
const value = useMemo(() => ({ a, b, c }), [a, b, c]);
<MyContext.Provider value={value}>
// 9) Selector pattern with use-context-selector
import { createContext, useContextSelector } from 'use-context-selector';
const UserContext = createContext({ name: '', email: '', role: '' });
function Name() {
const name = useContextSelector(UserContext, (v) => v.name); // only re-renders on name change
return <span>{name}</span>;
}
// 10) Testing components that use context
import { render } from '@testing-library/react';
render(<ThemeContext.Provider value={{ theme: 'dark', toggle: () => {} }}><Button /></ThemeContext.Provider>);
// 11) Context vs prop drilling vs state library
// Context — multi-component value that rarely changes (auth, theme, locale)
// Props — direct parent → child (2-3 levels)
// State lib (Zustand/Redux) — frequently-updating shared state with selectors
// 12) Server state — DON'T use context
// Use TanStack Query / SWR / Apollo / RTK Query. They handle cache + revalidation.
// 13) Common bugs
// • New value object each render → every consumer re-renders; useMemo the value
// • Forgot to wrap in Provider → consumers get the default value (often surprising)
// • Context value not stable across renders → useMemo + useCallback for functions
// • Trying to put context on hot paths (mouse position, animation) → use Zustand instead
// • Wrapping the whole app in one giant context → split by concern
// • Reading context outside a function component → can't; only inside renders or hooks
// • Forgetting that consumers re-render based on REFERENTIAL equality of value, not deep equality
Why it matters
Context replaces prop drilling for app-wide values that rarely change — theme, auth, locale, feature flags. Memoise the value object, split state vs dispatch for performance, reach for use-context-selector when you need true selector subscriptions, and stash server state in TanStack Query rather than context. For hot paths (cursor position, animation), pick Zustand or Jotai.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
const Theme = createContext('light');
<Theme.Provider value="dark"><App /></Theme.Provider>
Try it Yourself »
Discussion
Loading…