iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

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…