React useContext Hook

Share state across React components without prop drilling using the useContext hook.

TL;DR

  1. 01Create a Context with createContext for shared data.
  2. 02Wrap components with a Provider to make data available.
  3. 03Use useContext to access data in any child component.

Tips

  1. 01Create custom hooks for contexts to simplify usage and add error checking for consumers.

Warnings

  1. 01Every context change causes all consumers to re-render — split contexts by concern to avoid performance issues.

Creating Context

  • Create a context to hold shared data.
    const ThemeContext = React.createContext();
  • Provide an initial value as a fallback when no provider wraps the consumer.
    const UserContext = React.createContext({
      user: null,
      setUser: () => {}
    });
  • Export the context for use in other components.
    export const ThemeContext = React.createContext();
  • Keep context creation in its own file for clarity.
    // theme-context.js
    import { createContext } from "react";
    export const ThemeContext = createContext("light");
  • Use TypeScript generics to type the context value.
    interface User { id: number; name: string; }
    interface UserContextType { user: User | null; setUser: (u: User) => void; }
    
    const UserContext = createContext<UserContextType | undefined>(undefined);

Creating a Provider

  • Wrap your app with a Provider component.
    function App() {
      const [theme, setTheme] = useState("light");
      
      return (
        <ThemeContext.Provider value={{ theme, setTheme }}>
          <Header />
          <Main />
          <Footer />
        </ThemeContext.Provider>
      );
    }
  • Provider passes value to all child components.
    function Layout({ children }) {
      const [user, setUser] = useState(null);
      
      return (
        <UserContext.Provider value={{ user, setUser }}>
          {children}
        </UserContext.Provider>
      );
    }
  • Extract the provider into its own component to keep App clean.
    export function ThemeProvider({ children }) {
      const [theme, setTheme] = useState("light");
      return (
        <ThemeContext.Provider value={{ theme, setTheme }}>
          {children}
        </ThemeContext.Provider>
      );
    }
  • Memoize the context value to avoid re-rendering every consumer.
    function UserProvider({ children }) {
      const [user, setUser] = useState(null);
      const value = useMemo(() => ({ user, setUser }), [user]);
      return (
        <UserContext.Provider value={value}>
          {children}
        </UserContext.Provider>
      );
    }
  • Nest multiple providers at the top of the tree.
    function App() {
      return (
        <AuthProvider>
          <ThemeProvider>
            <Router>
              <AppRoutes />
            </Router>
          </ThemeProvider>
        </AuthProvider>
      );
    }

Consuming Context

  • Access context with useContext hook.
    function Header() {
      const { theme, setTheme } = useContext(ThemeContext);
      
      return (
        <button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
          Current theme: {theme}
        </button>
      );
    }
  • useContext works in any descendant component, no matter how deep.
    function DeepComponent() {
      const { user } = useContext(UserContext);
      return <p>User: {user?.name}</p>;
    }
  • Returns the createContext default value if no matching Provider is above it, or undefined if no default was set.
  • Destructure only the values you need to be explicit.
    // Clear: names the exact values this component uses
    const { theme } = useContext(ThemeContext);
  • Re-renders happen whenever the context value changes.
    // This component re-renders every time theme or setTheme changes
    const { theme, setTheme } = useContext(ThemeContext);

Custom Hooks

  • Create a custom hook to simplify context access.
    function useTheme() {
      const context = useContext(ThemeContext);
      if (!context) {
        throw new Error("useTheme must be inside ThemeProvider");
      }
      return context;
    }
    
    function Header() {
      const { theme, setTheme } = useTheme();
      return <button onClick={() => setTheme("dark")}>{theme}</button>;
    }
  • Custom hooks provide error checking and a cleaner API.
  • Makes consuming context easier for other developers.
  • Add computed values or helpers inside the custom hook.
    function useAuth() {
      const { user } = useContext(AuthContext);
      return {
        user,
        isLoggedIn: !!user,
        isAdmin: user?.role === "admin"
      };
    }
  • Export only the custom hook, not the raw context.
    // users import useUser, not UserContext directly
    export { useUser };       // export hook
    // don't export UserContext — keep it private

Best Practices

  • Place the Provider as close as possible to the components that need it, not always at the app root.
    // Scope SidebarContext to the sidebar, not the whole app
    function Sidebar() {
      return (
        <SidebarContext.Provider value={collapsed}>
          <SidebarPanel />
        </SidebarContext.Provider>
      );
    }
  • Colocate context with the feature that owns it instead of lifting everything to a shared global file — move it up only when a second, unrelated feature genuinely needs the same data.
    // features/cart/cart-context.jsx — lives next to the feature, not in a top-level contexts/ folder
    export const CartContext = createContext(null);
  • Avoid context for state that changes on every keystroke or frame, like form input or animation values — high-frequency updates re-render every consumer and are a poor fit for the context model.
    // Fast-changing values fit local state or a state library better than context
    const [query, setQuery] = useState(""); // keep local, not in context
  • Prefer local state or props for data used by only 1–2 components.
    // Don't reach for Context just to avoid one prop
    // Context is useful for truly global data: auth, theme, locale
  • Test context consumers by wrapping them in the provider, not by mocking the context module.
    test("shows username from context", () => {
      render(
        <UserContext.Provider value={{ user: { name: "Alice" }, setUser: jest.fn() }}>
          <UserDisplay />
        </UserContext.Provider>
      );
      expect(screen.getByText("Alice")).toBeInTheDocument();
    });
  • Build a small reusable test helper that wraps render with your real providers, so every test exercises the same provider tree your app uses in production.
    function renderWithProviders(ui) {
      return render(<AuthProvider><ThemeProvider>{ui}</ThemeProvider></AuthProvider>);
    }

FAQ