# ADR 021: State-Management Strategy

**Status**: Accepted
**Date**: 2026-05-22
**Deciders**: Architecture Lead, Platform PO
**Closes**: KAR-531 (KAR-522 Phase 4)

---

## Context

The web app currently mixes state-management patterns ad-hoc:

- 136 files use `useState` / `useReducer` directly.
- 6 files use Context (UserSession, MasterData, Toast, Theme, I18n, OfflineProvider).
- No global store library (Zustand, Jotai, Redux) is installed.
- No server-state library (TanStack Query, SWR) is installed — server state is fetched in RSC and forwarded as props.
- No form library (React-Hook-Form, Formik) is installed — forms roll their own controlled state.

This works for the current scope but produces three recurring frictions:

1. **Form code drifts**: each form component re-implements validation, error mapping, and submission flow. Bugs (stale error state, race conditions on submit) reappear in different shapes.
2. **State category is unclear to new code**: the next developer (or AI agent) does not have a written rule for *which* tool to reach for. Default behavior is "wrap it in another Context", which adds re-render cost and obscures testability.
3. **Server-cache patterns are implicit**: data refresh after mutation uses a mix of `router.refresh()`, `revalidatePath()`, and full-reload. There is no shared mental model for "what does this app cache, and how does it invalidate".

The KAR-522 architecture audit (Source 14 — bulletproof-react) recommends a 5-category state model that distinguishes by *origin* and *lifetime* of the data, not by *where it is used*.

## Decision

State is classified into five categories. Each category has a single recommended tool. Deviations require an ADR amendment.

| Category | Origin / Lifetime | Recommended tool |
|---|---|---|
| **Component state** | Lives in one component, dies with unmount | `useState` / `useReducer` |
| **Server-cache state** | Originates server-side, mirrored on client | Next.js RSC + Server Actions; **TanStack Query only where streaming/optimistic UX requires client cache** |
| **Form state** | Field values + validation + submission lifecycle | **React-Hook-Form + Zod** (to be introduced — see KAR-530) |
| **Application state** | Cross-component, cross-route, in-memory only | App-wide React Context for *truly* global concerns (theme, locale, session); **Zustand for new global stores** (deferred until first real need) |
| **URL state** | Reflected in the address bar, sharable | Next.js `useSearchParams`, route params, `Link` / `router.push` |

### Per-category rules

**Component state**

- Default for anything that does not need to be shared. If you can lift state down, do it.
- Prefer `useReducer` once a component manages more than three coupled values.
- Never mutate state objects in place. Spread or `produce` (Immer is **not** a default dependency).

**Server-cache state**

- Default: read in a Server Component, pass to children as props. No client fetch.
- Mutate via Server Action, then `revalidatePath()` / `revalidateTag()` to invalidate the affected route segment.
- Reach for TanStack Query *only* when (a) the data must update without a server round-trip (optimistic UI), or (b) the same data is needed across unrelated client islands. Document the introduction in the feature's PR.
- Do not store server data in a Context "to avoid re-fetching" — that is what RSC + Next.js cache already do.

**Form state**

- New forms use React-Hook-Form with a Zod schema for validation. The Zod schema lives next to the Server Action that consumes it so both sides validate against the same source.
- Existing forms keep their hand-rolled state until they are touched for a non-trivial reason; convert during that touch.
- One abstract `<Form>` wrapper (to be introduced with KAR-530) standardises submission state, error display, and disabled state during submit.

**Application state**

- Context is reserved for app-wide concerns whose value does not change frequently: session, theme, locale, branding, toast queue, offline status. Today's six contexts (UserSession, MasterData, Toast, Theme, I18n, OfflineProvider) stay as Context.
- For *new* global stores with frequent updates, the default is **Zustand**. Adding Zustand requires:
  - A concrete cross-component requirement that Lifting State Up does not solve.
  - The store living under `lib/<feature>/store.ts` (module layer, ADR 015).
  - A test asserting the store's reducer/actions.
- Do **not** introduce Redux, Jotai, or MobX without a fresh ADR.

**URL state**

- Filters, sort order, pagination, and view-mode that should survive reload or be sharable go into URL search params via `useSearchParams`.
- Use `nuqs` *only* if/when typed URL state becomes a recurring need; until then, plain `useSearchParams` + manual parsing is enough.

## Rules this decision creates

1. PR review checks that new state lives in the right category. A `useState` for data that is actually URL-derived is flagged.
2. New global stores (any new Context or store) require a one-line note in the PR description: which category, why this tool.
3. New forms without React-Hook-Form + Zod require a justification (e.g. a one-field intake with no validation — fair).
4. Server data fetched on the client without a documented reason fails review.

## Migration

This ADR is **forward-looking**, not a forced rewrite.

- Existing useState code stays.
- Existing forms stay until touched.
- Existing six Contexts stay.
- Existing absence of TanStack Query stays — adoption is opt-in per feature.

What changes is the *next* state introduced: from this PR onward, the category decision is explicit.

## Consequences

### Accepted

- Some up-front cost when introducing the next form (React-Hook-Form learning curve).
- A second Zod schema location convention (next to the Server Action, not in a central `schemas/` folder).

### Forbidden

- Wrapping server-side data in a Context "to share it".
- Adding Redux / Jotai / MobX without a fresh ADR amending this one.
- Hand-rolled form state in a new component that has validation, async submission, or more than three fields.

## References

- KAR-522 Architecture Audit · Source 14 (bulletproof-react)
- KAR-522 Architecture Audit · Source 10 (React.dev)
- KAR-522 Cross-Synthesis · Pattern A (Modular Feature-Slice) + Pattern B (Observability)
- ADR 015 (Architecture boundary enforcement) — module layer where stores live
- ADR 019 (New module golden path) — wires this into the module scaffold
- KAR-530 (Bulletproof-react folder refactor) — companion change introducing `<Form>` wrapper
