Reference · Frontend for backend developers
The decision you will make more often than any other, as a ladder. Climb only as far as you are forced to.
Start at rung 1. Move up only when the current rung is genuinely impossible — not when it feels tedious. Most real code should sit on rungs 1–3.
useState in the componentOnly this component cares. An input's text, whether a dropdown is open, a hover flag.
Climb when: a second component needs the same fact.
Two or more components share the fact. Move it to the nearest ancestor of all of them; pass the value down as a prop and a setter down as a callback.
Climb when: it is being relayed through four-plus components that never use it.
childrenBefore adding any tool: pass JSX instead of data. Build the element where
the data already lives; the middle layers just render
{children} and never learn the prop exists.
Most drilling complaints end here. Try this before rung 4, every time.
Wide, deep, rarely-changing values: theme, locale, signed-in user, permissions. Not form values. Not server data.
Climb when: you need caching, derived state across pages, or fine-grained subscriptions.
Zustand, Jotai, Redux. Genuinely global client state with complex update logic. Rarer than the internet suggests.
Server data is not on this ladder at all. Data belonging to
a database has different needs — caching, loading and error states, refetching,
staleness, deduplication. It goes in a route loader (TanStack
Start) or TanStack Query. Putting it in useState,
Context or Redux means reimplementing all of that by hand, badly. This is the
single most common architectural mistake in React codebases.
| Ask | If yes |
|---|---|
| Can I calculate it from existing state or props? | It is not state. Compute it during render. Storing it creates a second truth that will drift. |
| Does it come from a database or API? | Not state. Loader or TanStack Query. |
| Does it change while the component is on screen, and does the UI depend on it? | It is state. Now apply the ladder. |
| Situation | Verdict |
|---|---|
| 1–3 layers | Fine. Explicit and readable. Leave it. |
| 4+ layers, one value | Try composition (rung 3) first. |
| 4+ layers, many values, many branches | Context is now reasonable. |
| Any depth, changes every keystroke | Never context. Keep it local or lift one level. |
The React docs' own position, worth quoting because it cuts against the folklore: "If your components are not trivial, it's not unusual to pass a dozen props down through a dozen components. It may feel like a slog, but it makes it very clear which components use which data." (Passing Data Deeply with Context)
| Smell | Usually means |
|---|---|
A useEffect that copies a prop into state | You created a second truth. Use the prop directly, or compute during render. |
A useEffect that syncs two pieces of state | One of them is derived. Delete it and compute it. |
| The same fact stored in two components | Lift it. They will disagree eventually — it is only a matter of when. |
| Context holding an object that changes often | Every consumer re-renders on every change. Split the context or move the value down. |
| A component that cannot render without a Provider | Its real dependency is invisible at the call site. Fine for theme; a problem for a reusable leaf component. |