React useContext

useContext lets any nested component read a value from a shared context without threading it through every intermediate component's props.

The Prop-Drilling Problem

In a normal component tree, data flows down through props. If a deeply nested component needs a value owned by a distant ancestor, every component in between has to accept and forward that prop, even if it never uses it itself. This pattern, called prop drilling, gets unwieldy as trees grow.

Creating and Providing Context

createContext(defaultValue) creates a context object with a Provider component attached. Wrapping part of your tree in <SomeContext.Provider value={...}> makes that value available to every descendant, no matter how deeply nested, via the useContext hook — with no props passed along the way.

  • Create a context object once with createContext, usually in its own module
  • Wrap the relevant part of the tree in that context's Provider and pass it a value
  • Call useContext(SomeContext) in any descendant that needs the value
  • Every consuming component automatically re-renders when the Provider's value changes

Creating and providing a theme context

import { createContext, useContext, useState } from "react";

const ThemeContext = createContext("light");

function Toolbar() {
  const { theme, toggleTheme } = useContext(ThemeContext);

  return (
    <div className={`toolbar toolbar-${theme}`}>
      <button onClick={toggleTheme}>
        Switch to {theme === "light" ? "dark" : "light"}
      </button>
    </div>
  );
}

function App() {
  const [theme, setTheme] = useState("light");

  function toggleTheme() {
    setTheme((prev) => (prev === "light" ? "dark" : "light"));
  }

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      <Toolbar />
    </ThemeContext.Provider>
  );
}

export default App;
export { ThemeContext };

Any descendant, however deeply nested, reads the current value with useContext(SomeContext) — no props required along the way. When the Provider's value prop changes, every component that consumes it re-renders with the new value.

Consuming the theme context

import { createContext, useContext, useState } from "react";

const ThemeContext = createContext("light");

function Toolbar() {
  const { theme, toggleTheme } = useContext(ThemeContext);

  return (
    <div className={`toolbar toolbar-${theme}`}>
      <button onClick={toggleTheme}>
        Switch to {theme === "light" ? "dark" : "light"}
      </button>
    </div>
  );
}

function App() {
  const [theme, setTheme] = useState("light");

  function toggleTheme() {
    setTheme((prev) => (prev === "light" ? "dark" : "light"));
  }

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      <Toolbar />
    </ThemeContext.Provider>
  );
}

export default App;
Note: Split unrelated state into separate contexts (ThemeContext, AuthContext) instead of one giant context — a component that only reads theme won't re-render when an unrelated user object changes.

Auth context with a custom hook

import { createContext, useContext, useState } from "react";

const AuthContext = createContext(null);

function AuthProvider({ children }) {
  const [user, setUser] = useState(null);

  function login(name) {
    setUser({ name });
  }

  function logout() {
    setUser(null);
  }

  return (
    <AuthContext.Provider value={{ user, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}

function useAuth() {
  return useContext(AuthContext);
}

function ProfileButton() {
  const { user, login, logout } = useAuth();

  if (!user) {
    return <button onClick={() => login("Ada")}>Log in</button>;
  }
  return (
    <div>
      <span>Welcome, {user.name}</span>
      <button onClick={logout}>Log out</button>
    </div>
  );
}

function App() {
  return (
    <AuthProvider>
      <ProfileButton />
    </AuthProvider>
  );
}

export default App;
export { AuthProvider, ProfileButton };
ApproachHow data flowsDownside
Prop drillingPassed explicitly through every intermediate component's propsMiddle components accept and forward props they never use themselves
ContextRead directly by any descendant via useContextEvery consumer re-renders when the context value changes, even for parts it doesn't use
Note: The value passed to createContext is only used when a component reads the context and no matching Provider exists above it in the tree — once you add a Provider anywhere above, it always wins over that default.

Exercise: React useContext

What core problem does React Context solve?