React useContext Hook

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

TL;DR

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

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>);
    }

Tips

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

Warnings

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

FAQ