name: state-management
# description: routing-facing summary of when this skill activates and what it covers.
description: "Use when deciding where state lives, how it propagates, and how it composes: local component state vs lifted/shared state vs application state, server state vs client state, URL as state, persistent state, derived state, and the cross-cutting decision of who owns which piece. Covers state colocation, lifting up, derivation vs duplication, single source of truth, optimistic updates, server-state cache invalidation (React Query/SWR model), URL state for deep-linking, and anti-patterns like prop-drilling, state sprawl, and global-state-by-default. Do NOT use for specific state library choice (Redux vs Zustand — tactical), data fetching mechanics (use api-design or rendering-models), client/server boundary (use client-server-boundary), distributed system state (use replication-patterns), or finite state machines (use state-machine-modeling)."
# license: SPDX-compatible license identifier for the skill content.
license: MIT
allowed-tools: Read Grep
# metadata: Skill Metadata Protocol fields encoded under Agent Skills-compatible frontmatter.
metadata:
# === v8 Classification (subject + public; polyhierarchy via subjects[]) — see ADR-0017 ===
# subject: primary browse shelf — what the skill teaches. One of twelve closed values:
# backend-engineering / frontend-engineering / software-architecture / data-engineering / agent-ops / ai-engineering /
# quality-assurance / design / reasoning-strategy / software-engineering-method / knowledge-organization / product-domain.
subject: frontend-engineering
# public: publishability/private-data gate. Boolean.
# true = publishable/shareable; false = private and excluded from public export.
# Project anchoring is carried separately by non-empty `project[]` plus `grounding`.
public: true
# scope: free-text PRD-style statement of what the skill teaches and what it excludes.
# (v8 required; not an enum). Mirrors Coverage + Do NOT Use When at frontmatter level.
scope: "Portable frontend state-placement discipline for deciding where each piece of application data lives, who owns it, how it propagates, and how it stays consistent across changes. Teaches the four-kinds classification (server state, client UI state, URL state, persistent state), the colocation-and-lifting decision path, single source of truth, server-state cache invalidation, URL state for deep-linking, and anti-pattern detection for prop drilling, duplicated state, state sprawl, and global-state-by-default. Portable across any frontend; principle-grounded, not repo-bound. Excludes tactical library choice, API surface design, client/server execution-boundary decisions, distributed-system replication, and finite-state workflow modeling."
# taxonomy_domain: optional hierarchical sub-path within `subject`. Slash-delimited
# lowercase kebab-case segments. rename of the original v8 `domain`. Remove when the flat
# `subject` is sufficient.
taxonomy_domain: engineering/frontend
# stability: lifecycle marker. One of:
# experimental (active development) / stable (production-ready) /
# frozen (no further changes expected) / deprecated.
# When `deprecated`, schema's allOf REQUIRES `superseded_by: <real-skill-name>`.
stability: experimental
# keywords: semantic phrases for fuzzy router activation. v8 cap: max 10.
# Keep terms a user would actually type when starting a task in this skill's domain.
keywords: ["state management","state colocation","lifting state","state derivation","single source of truth","server state","client state","URL state","persistent state","ephemeral state"]
# triggers: explicit-match activation phrases the router fires on literally.
# Use when label-based routing is intended; usually keywords + examples are enough.
triggers: ["where should this state live","should this be in component state or global","I have state across multiple routes","this prop is drilled through 5 components","is this server state or client state","should this be in the URL"]
# examples: 2-5 realistic user prompts the skill SHOULD activate for.
# Written in the user's voice. Improves retrieval recall beyond keywords alone.
examples: ["decide where the filter/sort/page state for a data table should live","decide whether a piece of state should be in the URL, component state, or persistent storage","diagnose whether a piece of duplicated state is a real performance need or accidental sprawl","structure state for a form that spans multiple steps and survives navigation"]
# anti_examples: near-miss prompts that should route ELSEWHERE.
# Pair with relations.suppresses (or legacy boundary alias) to name the confusable territory's owner.
anti_examples: ["implement a specific Redux reducer (tactical, library-specific)","design the JSON shape of an API response (use api-design)","model the state transitions of a multi-step workflow (use state-machine-modeling)","configure HTTP cache headers on the server (use rendering-models or http-semantics)"]
# relations: typed graph edges to sibling skills. Current fields:
# related (adjacency for browse / co-routing expansion) /
# suppresses (exclude listed skills from co-routing when THIS skill wins; write reason
# as "I own this exclusively over X", not "use X instead") /
# boundary (DEPRECATED alias of suppresses, retained for unmigrated skills) /
# verify_with (cross-check; co-loaded as one-hop expansion) /
# depends_on (composition; transitive — A→B→C loads all three) /
# broader / narrower (SKOS-style generalization) /
# disjoint_with (mutual exclusion for incompatible ownership).
relations: "{\"related\":[\"rendering-models\",\"client-server-boundary\",\"frontend-architecture\",\"api-design\",\"state-machine-modeling\"],\"suppresses\":[{\"skill\":\"client-server-boundary\",\"reason\":\"client-server-boundary owns the line between code-that-runs-where (server components, client components, the serialization boundary); this skill owns the orthogonal question of which side owns which piece of state. They compose: the boundary skill says what code runs where; this skill says what state lives where.\"},{\"skill\":\"rendering-models\",\"reason\":\"rendering-models owns the question of when content is generated (SSR, RSC, CSR, ISR); this skill owns the question of where data backing that content lives. The two intersect in server state: data fetched on the server that the client may still need to cache, invalidate, or localize.\"}],\"verify_with\":[\"rendering-models\",\"api-design\",\"state-machine-modeling\"]}"
# === Understanding fields (when comprehension_state: present) ===
# mental_model: the primitives of the concept and how they relate. One paragraph.
mental_model: |
State is not one thing — it is a category with FOUR distinct kinds: SERVER state (owned by backend, has cache invalidation, time-and-event-based revalidation, lives in React Query / SWR / RTK Query / Apollo / Server Components); CLIENT UI state (ephemeral within a render lifecycle, owned by component or hook in the active session, lives in useState / useReducer / Context / Zustand / Jotai); URL state (encoded in the URL itself, lifetime = until navigation, framework-router-owned); PERSISTENT state (browser local storage / IndexedDB / cookies, lifetime = until cleared, manual invalidation by app code). Each has different lifetimes, invalidation rules, and consistency requirements. Layered on top: the colocation default (Kent C. Dodds) — start local, lift only when needed, use Context for tree-scoped values, reach for a global store only when many components depend on many slices of a value that changes often. Single source of truth — never have two places that both claim to own one value.
# purpose: the problem this concept solves and why the field exists. One paragraph.
purpose: |
Replaces "global store for everything" or "useState everywhere" with classification-driven placement. Without state-management discipline, most recurring frontend bugs (broken back-button, stale data, prop-drilling, "why is this re-rendering," changes not reflected everywhere) are violations of the four-kinds-of-state classification: server data in a general-purpose store loses automatic revalidation; URL-worthy state in component state breaks the back-button; persistent state in component state loses on refresh; globally-shared state that should be local re-renders everything on every change. The discipline asks "which kind is this?" before "where should it live?" and reaches for the tool best suited to the kind, not a one-size-fits-all store.
# concept_boundary: what this concept is NOT. Distinguishes from adjacent skills by naming
# the MECHANISM that differs, not just the label. Canonical replacement for top-level `boundary`.
concept_boundary: |
Distinct from client-server-boundary, which owns the SERIALIZATION line between code that runs where (`'use client'`, `'use server'`) — state-management owns the orthogonal question of which side owns which piece of state; the two compose (boundary says what code runs where, state-management says what state lives where). Distinct from state-machine-modeling, which owns the finite-state representation of workflows (states, transitions, guards, side effects) — state-management owns the decision of WHERE state of any kind lives; the machine names values, this skill names locations. Distinct from api-design, which owns the request/response shape of the external API — state-management owns where the response data lives once it arrives and how it is invalidated. Distinct from rendering-models, which owns when content is generated (SSR / RSC / CSR / ISR) — state-management owns where the data backing that content lives.
# analogy: one-sentence metaphor preserving the core mechanism.
analogy: "State management is to a frontend application what addressing is to a postal system — you do not ask 'should this letter exist?', you ask 'where does it live?', and the right destination depends entirely on the letter's kind: a registered package (server data with provable delivery), a postcard (URL state, public on the back), a private letter (client UI state, inside an envelope), a deed (persistent state, kept in the safe). One mailbox for everything produces predictable failures."
# misconception: the wrong mental model people bring; corrected explicitly.
misconception: |
The wrong mental model is that state is one thing and the question is "which library should hold it?" It is not. State is at minimum FOUR kinds with different lifetimes and invalidation rules. Picking a library before classifying produces the same library used wrongly for all four kinds — Redux for server data (you reinvent React Query badly), Zustand for URL-worthy filter state (you break the back-button), Context for everything (you re-render the world on every change). The discipline is to classify first (server / client UI / URL / persistent), then pick the right tool per kind, then ask "do I even need a library, or is colocation enough?" Most state should live locally with the component that uses it; a global store is the exception, not the default.
Concept of the skill
What it is: State management is the frontend architecture discipline of deciding where each changing value lives, who owns it, how it propagates to consumers, and how it stays consistent over time.
Mental model: State is at least four kinds with different owners and lifetimes: server state, client UI state, URL state, and persistent state. Classify the value first, then choose the narrowest owner and tool that fits its lifetime and invalidation needs.
Why it exists: Most recurring frontend state bugs come from putting the right value in the wrong place: server data in a general-purpose store, URL-worthy state in component state, duplicated values in two owners, or global stores used before local state has proven insufficient.
What it is NOT: It is not tactical state-library selection, API schema design, finite-state workflow modeling, distributed replication, or the client/server execution-boundary decision. Those skills can compose with this one after the state kind and owner are clear.
Adjacent concepts: Rendering models, client-server boundary, frontend architecture, API design, state-machine modeling, server-state caching, and URL state.
One-line analogy: State management is like addressing mail: each item needs the right destination for its kind, and one mailbox for everything creates predictable delivery failures.
Common misconception: The wrong starting question is "which state library should hold this?" The right first question is "what kind of state is this, who owns it, and how long should it live?"
State Management
Coverage
The architectural discipline of deciding, for each distinct piece of data an application reads or writes, where it lives, who owns it, how it propagates, and how it stays consistent. Covers the four kinds of state (server, client UI, URL, persistent), the colocation default and the lifting move, the single-source-of-truth principle, the React Query / SWR doctrine for server state, the URL as a state container, the architectural anti-patterns of premature globalization and state sprawl, optimistic-update trade-offs, and the framing of state ownership as a design contract distinct from state-library selection.
Philosophy of the skill
State management is a series of location and ownership decisions, each of which has a correct default. The defaults are: colocate locally; lift only when needed; put server state in a server-state library; put URL-worthy state in the URL; put session-survival state in persistent storage; never have two sources of truth for one value. Most recurring frontend bugs (broken back-button, stale data, prop drilling, "why is this re-rendering," changes that don't reflect everywhere) are violations of these defaults. The discipline is not the library; it is the application of the defaults.
The discipline matters because state is not one thing — server state, client UI state, URL state, and persistent state have different lifetimes, invalidation rules, and consistency requirements. Treating them as one category (one store, one set of patterns) produces a codebase that gets all four kinds of state slightly wrong simultaneously. Treating them as four categories produces a codebase where each kind is handled by the tool best suited to it.
For agents writing or reviewing frontend code, the discipline is the framework that lets the agent reason locally — pick up a piece of state, classify it (server / client / URL / persistent), check its current location against the right location for its kind, and make targeted fixes. Without the framework, the agent pattern-matches against whatever the codebase already does, replicating existing choices without questioning whether they were correct.
The Four Kinds Of State
| Kind | Owned by | Lifetime | Invalidation rule | Common tools |
|---|
| Server state | A backend | Session, with revalidation | Time-based (stale-while-revalidate); event-based (mutation, focus, reconnect); manual | React Query, SWR, RTK Query, Apollo, Server Components |
| Client UI state | A component or hook in the active session | Ephemeral within a render lifecycle | Recreated on unmount | useState, useReducer, Context, Zustand, Jotai |
| URL state | The URL itself; framework router | Until URL changes | URL navigation | useSearchParams, route params, nuqs |
| Persistent state | The browser's local storage | Until cleared | Manual via app code | localStorage, IndexedDB, cookies |
Each piece of state in an application falls into one of these. Misclassifying produces predictable bugs — server data in a general-purpose store loses automatic revalidation; URL-worthy state in component state breaks the back-button; persistent state in component state loses on refresh.
The Colocation Rule (Kent C. Dodds)
"State should live as close as possible to where it's used." — colocation, deliberate lifting, escalation only by need.
The decision procedure:
- Start local. Put new state in the component that needs it. Use
useState or useReducer. - Two consumers? Lift. When a second component needs to read or write the state, lift to the nearest common ancestor and pass by props. Track the prop's path; if it goes through ≥4 intermediate components, consider Context or composition.
- Tree-scoped? Context. When the state is genuinely shared by a sub-tree (theme, viewer, auth) and the value changes rarely, use Context. Use
useContextSelector or pattern-match to slice if many consumers depend on different parts of a large value. - Application-wide with frequent fine-grained updates? Store. When you have many components depending on many slices of a value that changes often, a state library (Zustand, Jotai, Redux Toolkit) provides selector-based subscriptions that minimize re-renders. This is the rare case, not the default.
- Server data? Server-state library. Always. Don't put server data in a general-purpose store.
- URL-worthy? URL state. When the state should survive refresh, be shareable, respond to back-button.
Premature jumps (skipping step 2 to land at step 4, or starting at step 4 by default) cause state sprawl. The cost of climbing the ladder later (refactoring local to lifted) is less than the cost of being too high up to start.
The Server-State Distinction
Server state has four properties that no general-purpose store handles by default:
| Property | What it requires | Why a server-state library wins |
|---|
| Remote canonical source | The client cache may diverge from the server | Built-in stale-while-revalidate, refetch on focus/reconnect |
| Deduplication | Two components asking for the same data should fetch once | Query-key cache prevents duplicate fetches |
| Staleness | Old data should be replaced | Time-based and event-based invalidation |
| Mutation-triggered invalidation | Updating data should refresh related queries | Mutate-then-invalidate pattern by query key |
The recurring failure: using Redux/Zustand for everything, then manually building "refetch on focus" middleware, manual deduplication, manual cache expiry, and manual mutation-invalidation rules. Each one of those is correct in concept; together they reimplement React Query, badly. The disciplined answer is to use a server-state library for server data and keep the general-purpose store (if any) for genuinely client UI state.
URL State Decision Rule
"Should two people opening this URL see the same view?"
If yes, the state belongs in the URL. If no, it doesn't.
| State | URL-worthy? | Why |
|---|
| Current filter, sort, page | Yes | Sharing a link reproduces the view; back-button works |
| Selected tab in a multi-tab panel | Often yes | Bookmark-able sub-view |
| Open/closed of a sidebar | Usually no | Personal preference, not the shared view |
| Modal open/closed | Sometimes | If it's a deep-linkable modal (sharable URL), yes; if it's transient confirmation, no |
| Form values mid-edit | No | Ephemeral; shouldn't survive refresh from a stranger's link |
| Pagination cursor | Yes | Restores state on refresh; shareable position |
| Search query | Yes | Canonical "what is this user looking for" |
| Currently-hovered element | No | Ephemeral; not part of the shareable view |
Anti-Patterns And Their Fixes
| Anti-pattern | Symptom | Fix |
|---|
| Premature globalization | Every new state goes into the global store; the store grows; nobody knows what's in it | Start local. Only lift on observed need. Audit the store for entries that haven't been read in N months. |
| Server data in a general-purpose store | Custom middleware for refetch, dedup, invalidation; bugs in each | Migrate server data to React Query/SWR/RTK Query. Keep the general store for UI state only. |
| URL-worthy state in component state | Back-button doesn't work; refresh loses filters; can't share a link | Move to useSearchParams or equivalent. Adopt nuqs for type-safe URL state. |
| Prop drilling through 7 layers | Tedious to maintain; refactors break | Context for tree-shared data; or composition (children-as-prop) to avoid intermediate awareness. |
| Duplicated state (same value, two locations) | Updates in one place don't reflect in the other | Identify the canonical owner; derive in the other location or sync explicitly. |
| Stored derived state without need | Caches go stale; bugs from "I updated X but Y still shows old" | Compute derived state at read time. Use useMemo only with measured perf evidence. |
| Optimistic updates for high-failure mutations | "Did that work? Oh no, it didn't" UX | Use pessimistic (loading then success/error) for low-confidence mutations; optimistic for high-confidence only. |
| localStorage for everything user-prefs | Server doesn't know on first render; no cross-device sync | Server-side store for prefs that affect SSR; URL state for session-scoped prefs; localStorage only when local-only is correct. |
Verification
After applying this skill, verify:
- [ ] Every distinct piece of state in scope has been classified into one of the four kinds (server / client UI / URL / persistent).
- [ ] Server data is held by a server-state library (React Query, SWR, RTK Query, Apollo, Server Components), not a general-purpose store.
- [ ] State that should survive refresh, be shareable, or respond to back-button is in the URL.
- [ ] No piece of state is duplicated (same value held in two independent locations) without an explicit synchronization mechanism and a recorded reason.
- [ ] Local state stays local; lifts have a documented justification.
- [ ] Context use is limited to genuinely tree-scoped data; large frequently-changing values that go through Context have been migrated to a store with selector-based subscriptions.
- [ ] Derived state is computed at read time; stored derived values have measured performance evidence.
- [ ] Optimistic updates are limited to high-confidence mutations; rollback paths exist where they are used.
- [ ] Persistent state choices match what the data actually needs (localStorage / IndexedDB / cookies / server-stored prefs).
- [ ] The state model is documented enough that a new contributor can answer "where does X live" in under a minute.
Do NOT Use When
| Instead of this skill | Use | Why |
|---|
| Choosing between Redux, Zustand, Jotai, Recoil, MobX, etc. | Library docs + framework conventions | Library choice is a tactical decision below this skill |
| Designing the JSON shape of an API endpoint | api-design | api-design owns the external request/response surface; this skill owns where the response lives once it arrives |
| Modeling the finite-state transitions of a workflow | state-machine-modeling | state-machine-modeling owns the workflow representation; this skill owns the location decision |
| Encoding/decoding URL search params, route params, hash | url-state-management | url-state-management owns the encoding patterns; this skill owns the upstream "should it be in the URL" decision |
| Building distributed state across services | replication-patterns | replication-patterns owns multi-replica consistency; this skill applies to single-client state |
| Deciding what runs on server vs client | client-server-boundary | client-server-boundary owns the code-location boundary; this skill owns the state-location decision orthogonal to it |
| Building the cache infrastructure of a server-state library | the library itself (React Query docs, etc.) | The library is the implementation of what this skill recommends; don't reimplement |
| Concurrency control across multiple users editing the same record | (no direct skill — out of scope) | Conflict resolution mechanisms (versioning, OT, CRDT) are a separate concern |
Key Sources
- Dodds, K. C. (2019). "Application State Management with React" and "State Colocation will make your React app faster". Foundational essays on the colocation rule and the lift-when-needed discipline; widely cited as the modern doctrine successor to Redux-by-default.
- TanStack. React Query overview and motivation. The canonical articulation of the server-state distinction: "React Query is often described as the missing data-fetching library for React, but in more technical terms, it makes fetching, caching, synchronizing and updating server state in your React applications a breeze."
- Vercel. SWR docs. The first widely-adopted server-state library to make stale-while-revalidate the default mental model in React.
- Abramov, D. (2019). "You Might Not Need Redux" and follow-ups. Redux's author articulating that Redux is not always the right answer; foundational for the modern "store-by-need, not by-default" doctrine.
- React Team. Sharing State Between Components and Choosing the State Structure. The official React docs' framing of lifting state up, single source of truth, and avoiding state duplication.
- Karlton, P. (attributed). "There are only two hard things in Computer Science: cache invalidation and naming things." The canonical statement of why server-state caching is not trivial — its difficulty is one of the two named hardest problems in the field.
- Marquardt, J. (2024). "URL as the state of your app". Modern articulation of the URL-as-state-container principle in the App Router era.
- Pelle, M. (2024). nuqs documentation. Type-safe URL state library; reference implementation of the URL-as-state pattern with serialization, parsing, and React integration.
- Fowler, M. (2003). Patterns of Enterprise Application Architecture. Addison-Wesley. The classic treatment of the Service Layer / Data Mapper / Identity Map / Unit of Work patterns; the Identity Map pattern is the original articulation of "deduplicate fetches by entity key" that server-state libraries operationalize.