Lesson 5 · Frontend for backend developers

Context, and when not to use it

A way to skip the tree. Powerful, and the most over-applied feature in React.

Context lets a component read a value provided by an ancestor at any depth, without every component in between passing it along. The intermediate components never mention it.

In your language

Context is a ContextVar, or a value bound for the duration of a request. It is dynamic scoping: the value depends on where in the tree you are called from, not on what you were passed. Everything good and everything dangerous about it follows from that.

The three moving parts

src/contexts/ThemeContext.tsx

import { createContext, useContext, useState } from 'react'

type Theme = 'light' | 'dark'

// 1. CREATE — the default is used only when there is no Provider above.
const ThemeContext = createContext<Theme>('light')

// 3. CONSUME — wrapped in a custom hook, which is the convention.
export function useTheme() {
  return useContext(ThemeContext)
}

// 2. PROVIDE
export function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [theme] = useState<Theme>('dark')

  return (
    <ThemeContext value={theme}>
      {children}
    </ThemeContext>
  )
}

Wrap the tree once:

<ThemeProvider>
  <App />
</ThemeProvider>

And any descendant, at any depth, reads it with no props involved:

export function Avatar() {
  const theme = useTheme()
  return <div className={theme === 'dark' ? 'bg-slate-800' : 'bg-white'} />
}

The rule for when to use it

Context suits data that is genuinely global to a subtree, read in many places, and changes rarely. The canonical list is short, and that shortness is the point:

Good fitWhy
Theme / localeRead everywhere, changes almost never.
Signed-in userRead in many places, changes on login/logout.
Routing stateSame shape — which is why the router provides it for you.
Bad fitWhy
Form field valuesChange on every keystroke. Every consumer re-renders on every keystroke.
Server data (lists, records)Needs caching, loading and error states, refetching. That is TanStack Query's job, not Context's.
State two siblings shareLift it to their parent. Context here is a sledgehammer.
"It saves me passing a prop"Not a reason. That is what props are.

What it actually costs

Context is not free, and the costs are not obvious until they bite:

A search box's text is needed by a results list two levels below it. Best solution?

The decision, in order
  1. Pass props. Two or three layers is fine — stop worrying about it.
  2. Restructure with children so the middle layers do not see the data. Most drilling complaints end here.
  3. Only then, Context — and only for wide, rarely-changing values.
  4. Server data is a different problem. Reach for TanStack Query, not any of the above.

Exercise

Two halves; the second matters more than the first.

A — build it. Add a ThemeContext to the task app with a working light/dark toggle. The toggle button lives in a header; the task rows read the theme and change colour. Note that no component between the provider and a task row mentions the theme.

B — argue against it. Then add a second context holding tasks and toggleTask, so TaskRow can stop taking props entirely. It will work. Now write three or four sentences on what you gave up. Bring them to Martin.

Have a go at B's answer before revealing.

This is the exercise. The skill being built is declining to use context, which is harder than using it.

Ask Martin. Ask which contexts exist in the real codebase and why each one earned its place. Then take the disagreement you are most unsure about to Reactiflux or r/reactjs — "when is context the wrong tool" is a question practitioners argue about productively, and the argument is where the judgement comes from.