Hirefullstack – Software Engineering & IT-Beratung aus Berlin
← Back to overview
Core 11 min read

useContext

Passing a value down without bothering every floor on the way

In one sentence

Context is a pneumatic tube through your components: put something in at the top, take it out anywhere below – without handing it down as a prop through five layers.

The problem has a name: Prop drillingPassing a value down through several components that do not need it themselves.. You want to get the theme from the very top down to a button at the very bottom – and you have to pass it through four components that do not need it themselves. That is what context solves. And nothing else.

Put another way

Props are like passing something hand to hand: everyone in the chain has to hold the parcel. ContextA kind of pneumatic tube: a value goes in at the top and can be taken out anywhere below. is the pneumatic tube: drop it in at the top, it arrives at the bottom – the floors in between never notice.

Three steps

theme.tsx
import { createContext, useContext, useState } from "react";

// 1) Create the tube
const ThemeContext = createContext<Theme | null>(null);

// 2) Put something in at the top
function App() {
  const [mode, setMode] = useState("light");
  const value = { mode, toggle: () => setMode(m => m === "light" ? "dark" : "light") };

  return (
    <ThemeContext.Provider value={value}>
      <WholePage />
    </ThemeContext.Provider>
  );
}

// 3) Take it out below – however deep
function Button() {
  const theme = useContext(ThemeContext);
  return <button onClick={theme.toggle}>{theme.mode}</button>;
}
Tip Always wrap it in your own hook

Instead of writing useContext(ThemeContext) in 30 places, you write useTheme() once. The benefit: the “is there even a provider?” check lives in one place, and you can rebuild the inside later without touching the callers.

own-hook.ts
export function useTheme() {
  const value = useContext(ThemeContext);
  if (!value) throw new Error("useTheme needs a <ThemeProvider> above it");
  return value;
}

Important: context stores nothing

Context only transports. The state still lives in an ordinary useState inside the component that renders the ProviderThe component that supplies a context value. Everything inside it can reach that value.. So context does not replace a state manager like Redux – it replaces passing props down.

Reach for it when …
  • +

    Theme, language, formatting

  • +

    Who is signed in and what they are allowed to do

  • +

    Values inside one component family (<Tabs><Tab>)

  • +

    In short: things that change rarely and that many need

Skip it when …
  • Values that change constantly (mouse position, every key press)

  • As a cache for server data – there are ready-made libraries for that

  • To save two levels of props – then the prop is simpler

The one trap: the object is new every time

everything below redraws every time
function App() {
  const [user, setUser] = useState(null);

  // this object is new on EVERY draw
  return (
    <UserContext.Provider value={{ user, setUser }}>
      <BigTree />
    </UserContext.Provider>
  );
}
stable as long as nothing changes
function App() {
  const [user, setUser] = useState(null);

  const value = useMemo(() => ({ user, setUser }), [user]);

  return (
    <UserContext.Provider value={value}>
      <BigTree />
    </UserContext.Provider>
  );
}

Every component reading the context redraws as soon as the value is a different one – and a freshly built object always is. You will meet useMemo in the useMemo & useCallback lesson.

In more depth: splitting, memo and Next.js optional
Tip Separate the rare from the frequent

A proven pattern: two contexts – one for the data (user), one for the actions (logout, setUser). The actions context never changes, so components that only need actions never redraw.

Worth knowing: React.memo does not help against context updates. The value does not arrive through props, it goes around them – so the prop check never sees it.

Good to know In Next.js the provider needs "use client"

createContext and useContext only work in a Client componentA component that runs in the browser and can therefore use state, hooks and clicks. Marked with "use client" at the top of the file.. So the provider goes in a file with "use client" at the top. You can still pass server components in as children – they keep running on the server.

Good to know React 19: shorter providers

Instead of <ThemeContext.Provider value={…}> you may now write <ThemeContext value={…}>. Both work.

Does it stick?

4 questions on this lesson. Wrong answers show up in your stats.

Now write it yourself

Get rid of the passing-down

mode and toggle are handed through three levels even though only the bottom one needs them. Replace that with a context plus a useTheme() hook.

  • Toolbar and Panel take no props any more
  • ThemedButton gets everything from useTheme()
  • A click switches between light and dark
import { createContext, useContext, useState } from "react";

// TODO: create the context and build a useTheme() hook

function ThemedButton({ mode, toggle }: any) {
  return (
    <button onClick={toggle} data-testid="btn">
      Mode: {mode}
    </button>
  );
}

function Panel({ mode, toggle }: any) {
  return <ThemedButton mode={mode} toggle={toggle} />;
}

function Toolbar({ mode, toggle }: any) {
  return <Panel mode={mode} toggle={toggle} />;
}

export default function App() {
  const [mode, setMode] = useState("light");
  const toggle = () => setMode((m) => (m === "light" ? "dark" : "light"));

  return (
    <div style={{ fontFamily: "system-ui", padding: 16 }}>
      <Toolbar mode={mode} toggle={toggle} />
    </div>
  );
}

Hirefullstack

Need React firepower on your team?

We have been building React and Next.js applications for clients across Germany for years – as a single expert, as reinforcement for an existing team, or as a complete Scrum team.

Talk about your project →