Context API provides a way to pass data through the component tree without having to pass props manually at every level (called "props drilling"). It's React's built-in solution for global/shared state.
How it works:
createContext(defaultValue)Provider component with a valueuseContext(TheContext)Full code example:
1import { createContext, useContext, useState, useMemo } from "react";23// 1. Create context with a meaningful default4const ThemeContext = createContext<{5 theme: "light" | "dark";6 toggleTheme: () => void;7}>(null!);89// 2. Provider component10function ThemeProvider({ children }) {11 const [theme, setTheme] = useState<"light" | "dark">("light");1213 const toggleTheme = () => setTheme(t => t === "light" ? "dark" : "light");1415 // Memoize to prevent unnecessary consumer re-renders16 const value = useMemo(() => ({ theme, toggleTheme }), [theme]);1718 return (19 <ThemeContext.Provider value={value}>20 {children}21 </ThemeContext.Provider>22 );23}2425// 3. Consumer component (any depth in the tree)26function ThemedButton() {27 const { theme, toggleTheme } = useContext(ThemeContext);28 return (29 <button30 onClick={toggleTheme}31 style={{ background: theme === "dark" ? "#333" : "#fff" }}32 >33 Current: {theme}34 </button>35 );36}
The performance problem:
When the Provider's value prop changes, ALL components that call useContext for that context re-render — even if they only use a small part of the value. This is the #1 performance issue with Context.
Optimization strategy 1 — Split contexts:
1// BAD: one giant context — every consumer re-renders on any change2const AppContext = createContext({ theme, user, settings, notifications });34// GOOD: split into focused contexts5const ThemeContext = createContext(theme);6const UserContext = createContext(user);7const SettingsContext = createContext(settings);89// Now ThemeContext consumers only re-render when theme changes10// UserContext consumers only re-render when user changes
Optimization strategy 2 — Memoize the Provider value:
1// BAD: new object every render = all consumers re-render2<ThemeContext.Provider value={{ theme, setTheme }}>34// GOOD: memoized object = only re-renders when theme changes5const value = useMemo(() => ({ theme, setTheme }), [theme]);6<ThemeContext.Provider value={value}>
Optimization strategy 3 — Use the "state reducer" pattern:
Provide only dispatch (which is stable across renders) via context, and let consumers read local state via useReducer. This way the Provider value never changes.
When to use Context vs. state management libraries: | Scenario | Use Context | Use Library (Zustand, Redux) | |---------|------------|------------------------------| | Few consumers | ✅ | Overkill | | Infrequent updates | ✅ | Overkill | | Complex state transitions | ❌ | ✅ | | Frequent updates causing perf issues | ❌ | ✅ | | Need DevTools / middleware | ❌ | ✅ |