# State Management Decisions

> Decision framework for choosing the right state location — URL, server cache, local component, or shared/global store. Use when state-sync bugs appear, prop drilling gets deep (3+ levels), filters/tabs lose state on reload, or quarterly review. Not for form state specifically (use form-ux) or when the state is actually server data (use api-caching-optimization).

- Skill: `jaykim88/state-management-decisions` (Agent Skill)
- Install (CLI): `npx skillmds@latest add jaykim88/state-management-decisions`
- Raw SKILL.md: https://api.skillmd.com/api/skills/jaykim88/state-management-decisions/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- License: MIT
- Author: JayKim88 (https://skillmd.com/u/jaykim88)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/jaykim88/state-management-decisions

---


# State Management Decisions

## Purpose
Classify each piece of state by source and place it in exactly one home. Most "state management problems" are actually misclassification problems — server state in a global store, UI state duplicated across components, derivable values stored as state.

**Universal** — the source-classification model (derivable / URL / server / local / shared) applies to any reactive framework; only the tool names per category differ.

## Procedure

1. **Classify every piece of state by source**

   | Source | What to do | Example |
   |---|---|---|
   | **Derivable** | Don't store — compute from input | `fullName` from `firstName + lastName` |
   | **URL-appropriate** | Sync to URL query params | filters, tabs, page number, modal open state for shareable links |
   | **Server data** | Server-state cache library | API responses, cached lists |
   | **Local component** | Framework's local state primitive | form draft, hover state, single-component UI |
   | **Shared / global** | Cross-component store with selector pattern | theme, current user object, cross-page UI state |

   - Buckets compose — don't force a whole feature into one: a search page's query is **URL state** that *drives* a **server fetch**; classify each layer (the query string vs. the fetched results) to its own home.

2. **Detect derivable state — the highest-leverage fix (eliminated state can't desync)**
   - Two patterns to find: (a) an effect that writes state derived from props/other state (`useEffect.*setX`), and (b) a state value that merely mirrors a prop or is recomputed each render
   - Replace with a computed value (memoize only if the computation is genuinely expensive)

3. **Identify missing URL state**
   - Tabs, filters, pagination, sort order, modal open/close (when shareable matters)
   - Symptom: refreshing the page loses the user's place
   - Fix: lift to URL via `nuqs` or `useSearchParams`

4. **Identify server-state-in-global-store**
   - Anti-pattern: `useStore` holds `users[]` fetched from API
   - Why bad: cache invalidation becomes your problem, no built-in stale/loading state
   - Fix: move to TanStack Query / SWR / Apollo

5. **Collapse related local-state atoms into one unit**
   - 3+ interdependent local-state values in one component → consolidate into a single reducer (or a small local store)
   - Especially for wizards and complex async sequences
   - **For forms specifically**: prefer a form library + schema validation over a hand-rolled reducer once you have > 3 fields or any validation (see `form-ux` skill)

6. **Move frequently-changing shared values off a broad-subscription primitive**
   - A shared value that changes often and is read by many components causes excessive re-renders if it lives in a broad-subscription context
   - Move it to a selector-based store so each consumer subscribes only to the slice it needs
   - Keep broad context for rarely-changing values: theme, locale, auth identity

7. **Apply optimistic UI for instant-feedback mutations**
   - Mutations where the user expects immediate feedback (like-button, comment-post, drag-reorder)
   - Render the optimistic result immediately; roll back on server error

8. **Document decisions**
   - For non-trivial state: comment why this tool was chosen
   - Optionally: ADR for the global state architecture

## Completion Criteria
- [ ] Derivable state = 0 (no `useEffect` syncing derived values)
- [ ] URL-appropriate state lives in URL (refresh-resilient)
- [ ] Prop drilling depth ≤ 2 (deeper → lift to a shared store or context)
- [ ] Server state in a server-state cache library (TanStack Query / SWR / Apollo), not a general client store
- [ ] State classification recorded for non-trivial cases

## Output
- **Refactored state code**: each piece classified and moved to its correct home (URL / server / local / shared)
- **ADR**: `docs/adr/ADR-NNN-state-management-strategy.md` if introducing or changing the global store choice (see `decision-records` skill)
- **Inline comments**: `// state: [server|url|local|shared] because [reason]` at non-obvious state declarations
- **PR description block**:
  ```
  - Derived state eliminated: N
  - URL state added: N (filter / tab / page / modal)
  - Server state moved to TanStack/SWR: N
  - Prop drilling fixed (max depth: was N → now ≤ 2)
  ```

## Implementation

### React + Next.js (default)
| Source | Tool |
|---|---|
| URL | `nuqs` (Next.js) or `useSearchParams` |
| Server | TanStack Query / SWR / Apollo |
| Local | `useState` / `useReducer` |
| Shared | Zustand (with selectors) |
| Optimistic mutation | `useOptimistic` (React 19) |

### Other stacks
- **Vue / Nuxt**: URL → `useRoute().query` + `definePageMeta`; Server → TanStack Query (Vue) / VueUse `useFetch`; Local → `ref`/`reactive`; Shared → Pinia (with `defineStore` + getters acting as selectors)
- **SvelteKit**: URL → `$page.url.searchParams` + `goto()`; Server → TanStack Query (Svelte) / SvelteKit `+page.ts` load; Local → Svelte 5 `$state` runes; Shared → Svelte stores or context API
- **Angular**: URL → `Router` + `ActivatedRoute.queryParams`; Server → TanStack Query (Angular) or RxJS service; Local → component property / signal; Shared → service with signals or NgRx for complex cases
- **Universal anti-pattern**: server state in a global store. Use a server-state library — every modern framework has TanStack Query bindings.

## Related skills
- `form-ux` — for form state specifically (use react-hook-form over useReducer)
- `api-caching-optimization` — when the state is actually server data
- `architecture-improvement` — for cross-module state placement

## Reference
- **Key insight encoded**: Classify state by source *before* picking a tool. Server state → TanStack Query (not Zustand — TanStack handles cache/stale/loading for free). URL state → nuqs (shareable, refresh-resilient). Local complex (forms/wizards) → useReducer. Cross-component client → Zustand. Reaching for a global store before this triage is an architectural smell.

