Reference · Frontend for backend developers

Where does this state go?

The decision you will make more often than any other, as a ladder. Climb only as far as you are forced to.

The shape of all React data flow

┌─────────────────┐ │ Parent │ owns state └────────┬────────┘ │ props ↓ │ ↑ callback invocation (data) │ (intent: "user clicked me") │ ┌────────┴────────┐ │ Child │ renders it, reports events └─────────────────┘ Data flows DOWN only. Nothing flows up. A child changes things by calling a function it was given.

The ladder

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.

  1. Local state — useState in the component

    Only this component cares. An input's text, whether a dropdown is open, a hover flag.

    Climb when: a second component needs the same fact.

  2. Lift it to the closest common parent

    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.

  3. Restructure with children

    Before 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.

  4. Context

    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.

  5. A state library — and only for client state

    Zustand, Jotai, Redux. Genuinely global client state with complex update logic. Rarer than the internet suggests.

Off to the side, not up the ladder

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.

Three questions that settle most cases

AskIf 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.

Prop drilling: the honest assessment

SituationVerdict
1–3 layersFine. Explicit and readable. Leave it.
4+ layers, one valueTry composition (rung 3) first.
4+ layers, many values, many branchesContext is now reasonable.
Any depth, changes every keystrokeNever 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)

Smells

SmellUsually means
A useEffect that copies a prop into stateYou created a second truth. Use the prop directly, or compute during render.
A useEffect that syncs two pieces of stateOne of them is derived. Delete it and compute it.
The same fact stored in two componentsLift it. They will disagree eventually — it is only a matter of when.
Context holding an object that changes oftenEvery consumer re-renders on every change. Split the context or move the value down.
A component that cannot render without a ProviderIts real dependency is invisible at the call site. Fine for theme; a problem for a reusable leaf component.