Lesson 5 · Frontend for backend developers
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.
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.
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'} />
}
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 fit | Why |
|---|---|
| Theme / locale | Read everywhere, changes almost never. |
| Signed-in user | Read in many places, changes on login/logout. |
| Routing state | Same shape — which is why the router provides it for you. |
| Bad fit | Why |
|---|---|
| Form field values | Change 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 share | Lift it to their parent. Context here is a sledgehammer. |
| "It saves me passing a prop" | Not a reason. That is what props are. |
Context is not free, and the costs are not obvious until they bite:
{ user, theme, sidebarOpen } re-renders every consumer
whenever the sidebar toggles.A search box's text is needed by a results list two levels below it. Best solution?
children so the middle layers do not see
the data. Most drilling complaints end here.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.
TaskRow's props no longer describe what it needs — its
real dependency became invisible.This is the exercise. The skill being built is declining to use context, which is harder than using it.