# State Management Patterns

Prerequisite: `frontend/react/hooks/01-usestate`, `04-usereducer`, and `frontend/react/advanced/01-context_api_in_depth`. This topic is not about a library — it's about the decision process for *where state should live* and *how it should be shaped*, before you ever reach for Redux or Zustand (`frontend/ecosystem/10-redux`, `12-zustand`).

## 1. Colocate by Default

The single highest-leverage rule: state lives as close as possible to where it's used, and moves up only when a sibling actually needs it.

```tsx
// BAD: hoisted to the top for no reason — App re-renders on every keystroke
function App() {
  const [query, setQuery] = useState("");
  return (
    <>
      <SearchBox query={query} onChange={setQuery} />
      <StaticFooter /> {/* re-renders on every keystroke too, for nothing */}
    </>
  );
}

// GOOD: colocated — only SearchBox's subtree re-renders
function App() {
  return (
    <>
      <SearchBox />
      <StaticFooter />
    </>
  );
}
function SearchBox() {
  const [query, setQuery] = useState("");
  return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
}
```

Every `useState` call re-renders the component that owns it (and its subtree, absent memoization). Placing state one level higher than necessary drags unrelated siblings into that re-render for free — see `frontend/react/advanced/03-performance_optimization` for measuring this.

## 2. Lifting State Up

State moves up exactly one level: to the nearest common ancestor of the components that need to share it — no further.

```tsx
function FilterableList() {
  const [filter, setFilter] = useState(""); // lifted here because BOTH children need it
  return (
    <>
      <FilterInput value={filter} onChange={setFilter} />
      <ItemList filter={filter} />
    </>
  );
}
```

"Lift to the common ancestor" is a ceiling, not a default — lifting further than that ancestor (e.g., all the way to `App`) reintroduces the colocation problem from Section 1 for no benefit.

## 3. Prop Drilling and Its Real Cost

Passing a prop through components that don't use it, just to reach one that does, is *prop drilling*. It is not free, but the cost is often mischaracterized:

```tsx
function Page({ user }: { user: User }) {
  return <Layout user={user} />; // Layout doesn't use `user`
}
function Layout({ user }: { user: User }) {
  return <Sidebar user={user} />; // Sidebar doesn't use `user` either
}
function Sidebar({ user }: { user: User }) {
  return <Avatar user={user} />; // finally used here
}
```

The real costs are **maintenance** (every intermediate component's signature must change when the shape changes) and **coupling** (`Layout` and `Sidebar` now mention `User` even though they don't care about it) — not rendering performance. `Layout` re-rendering because `user` changed is no different, performance-wise, from `Layout` re-rendering for its own reasons. Reach for context (Section 4, and the full mechanism in topic 01) when the drilling depth or the number of intermediate components makes the maintenance cost outweigh context's re-render tradeoffs — not automatically at the first sign of drilling.

## 4. Reducer-Per-Feature

When several pieces of state change together in response to the same events, a `useReducer` with a discriminated-union action type keeps the transitions in one auditable place instead of scattered across several `setState` calls that must always be called together.

```tsx
type FormState = { values: Record<string, string>; errors: Record<string, string>; submitting: boolean };
type FormAction =
  | { type: "change"; field: string; value: string }
  | { type: "submitStart" }
  | { type: "submitError"; field: string; message: string }
  | { type: "submitSuccess" };

function formReducer(state: FormState, action: FormAction): FormState {
  switch (action.type) {
    case "change":
      return { ...state, values: { ...state.values, [action.field]: action.value } };
    case "submitStart":
      return { ...state, submitting: true, errors: {} };
    case "submitError":
      return { ...state, submitting: false, errors: { ...state.errors, [action.field]: action.message } };
    case "submitSuccess":
      return { ...state, submitting: false };
  }
}
```

Three separate `useState` calls (`values`, `errors`, `submitting`) can drift out of sync — e.g., a bug path that sets `submitting: false` but forgets to clear `errors`. A reducer makes every valid transition an explicit, testable case, and the switch is exhaustive-checkable by TypeScript when `FormAction` is a closed union.

## 5. Derived State: Compute, Don't Store

A recurring bug source is storing a value that is fully computable from other state, then forgetting to update it when the source changes.

```tsx
// BAD: fullName is redundant state that can go stale
const [firstName, setFirstName] = useState("Ada");
const [lastName, setLastName] = useState("Lovelace");
const [fullName, setFullName] = useState("Ada Lovelace"); // must be kept in sync manually

// GOOD: derive it during render — impossible to go stale
const [firstName, setFirstName] = useState("Ada");
const [lastName, setLastName] = useState("Lovelace");
const fullName = `${firstName} ${lastName}`; // recomputed every render, always correct
```

The rule of thumb: if a value can be computed from existing state and props with a pure function, it is **not state** — it's a render-time calculation. Wrap it in `useMemo` only after profiling shows the computation itself is expensive (`frontend/react/hooks/06-usememo`); `useMemo` does not change correctness here, only avoids recomputation cost.

## 6. State Machines for Impossible States

A handful of independent booleans can represent combinations that make no sense (`isLoading: true, isError: true` at the same time). A discriminated union collapses the state space to only the combinations that are actually valid.

```tsx
// BAD: 2^3 = 8 possible boolean combinations, most of them nonsensical
interface BadState { isLoading: boolean; isError: boolean; data: Data | null }

// GOOD: exactly the valid states, and TypeScript enforces exhaustive handling
type FetchState =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: Data }
  | { status: "error"; message: string };

function render(state: FetchState) {
  switch (state.status) {
    case "idle": return "Waiting to start";
    case "loading": return "Loading...";
    case "success": return state.data; // TS knows `data` exists ONLY here
    case "error": return state.message; // TS knows `message` exists ONLY here
  }
}
```

`state.data` is inaccessible outside the `"success"` branch and `state.message` outside `"error"` — the type system, not a runtime check, prevents rendering a spinner and an error message from the same render.

## 7. Under the Hood: Batching and the Update Queue

![Two setState calls inside one handler enqueue two updates on the same fiber; React processes both before committing a single re-render](diagrams/01-batched-updates.svg)

When an event handler calls `setState` twice, React does not re-render twice:

```tsx
function Counter() {
  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);
  function handleClick() {
    setCount((c) => c + 1); // enqueued
    setFlag((f) => !f);     // enqueued
    // still only ONE re-render after this handler returns
  }
  return <button onClick={handleClick}>{count}</button>;
}
```

Each `setState` call appends an **update object** (`{ action, next, eagerState }`) to that hook's own circular update queue on the fiber — it does not synchronously mutate `count` or trigger a render. React marks the fiber (and its ancestors, via `lanes`/`childLanes` exactly as in topic 01's propagation) as having pending work and schedules a single work loop pass. Inside that pass, `updateReducer` walks each hook's queue *in order*, applying every pending action to compute the final `memoizedState`, and only then commits one DOM update. This is why `setCount(count + 1); setCount(count + 1)` inside one handler produces `+1`, not `+2` — both updates close over the same stale `count` read at call time — while the functional form `setCount(c => c + 1)` reads the *previous update's result* off the queue rather than the stale closure, and correctly produces `+2`. Batching used to be limited to React event handlers before React 18; since 18, `flushSync` is the escape hatch to force a synchronous, unbatched render when interop code needs the DOM updated immediately.

This is also the underlying reason context (identity-based, whole-value propagation) and external stores (subscription + selector, described in `frontend/ecosystem/10-redux` and `12-zustand`) *feel* different but are both built on the same primitive: something marks a fiber's lanes dirty, and the work loop decides what to re-render from there.

## 8. Common Pitfalls

1. **Lifting state to the root "just in case."** Every level between the state and its farthest consumer re-renders on every change even if nothing in between reads it — the fix is lifting only to the actual common ancestor (Section 2), not to `App`.
2. **Storing derived values instead of computing them.** Two pieces of state that must be kept manually in sync (Section 5) will eventually be updated in the wrong order or only one will be updated — computed values can't go stale because there's nothing to forget to update.
3. **Independent booleans that model dependent state.** `isLoading`/`isError`/`hasData` as three separate `useState` calls allow simultaneous `true`/`true`/`true`, a state your UI was never designed to render — collapse them into one discriminated union (Section 6).
4. **Splitting one logical update across multiple `setState` calls that read stale closures.** `setX(x + 1); setY(x + 1)` inside the same handler uses the *same* stale `x` for both — reach for the functional updater form or, better, a single reducer action that updates both fields from one piece of logic.
5. **Reaching for a reducer before there's more than one transition.** A reducer with a single action type is strictly more code than the equivalent `useState` for no benefit — introduce it when a second related action type shows up, not preemptively.
6. **Mistaking prop drilling depth for a rendering problem.** Passing an unused prop through three components costs a few extra lines of typing, not three extra re-renders that wouldn't have happened anyway (Section 3) — don't reach for context purely to shorten a prop chain that isn't otherwise a re-render bottleneck.

## 9. Quick Reference

| Situation | Pattern |
|---|---|
| State used by one component | Colocated `useState` in that component |
| State used by a few siblings | Lift to their nearest common ancestor |
| State used by many components across the tree | Context (see topic 01) or external store |
| Several fields that always change together | `useReducer` with a discriminated-union action type |
| A value fully computable from other state/props | Compute during render, don't store it |
| A handful of dependent booleans | Discriminated union modeling only the valid states |
| Batching several `setState` calls in one handler | Automatic since React 18; use functional updaters for sequential math |
| Need a synchronous, unbatched render | `flushSync` (rare — interop escape hatch) |

Next: `frontend/react/advanced/03-performance_optimization` covers measuring and fixing the re-render costs these placement decisions create.
