State Management
Prefer React state before reaching for Zustand or Jotai — what to use when, and where state lives.
The Vesperia policy is simple: prefer React's built-in state before reaching for a state library. Most state needs are met by useState, useReducer, and context. Zustand or Jotai come out only when those genuinely fall short.
The biggest source of accidental complexity in a frontend is putting state in a global store that didn't need to be there. Start local; escalate only when you feel real pain.
First: is it even client state?
Before choosing a state tool, ask whether the data is server state. Anything fetched from the API — lists, records, the "current" entity — belongs in the TanStack Query cache, not in a store. See Data Fetching. Keeping server data in a client store leads to stale copies and manual syncing.
What's left is genuine client state: form inputs, toggles, selections, wizard steps, UI modes.
The escalation ladder
Reach for the simplest tool that fits, in this order:
useState— local, independent values. The default.useReducer— when several values change together or transitions have rules (wizards, complex forms, toggling related flags). Inidxnet-web, small grouped UI state often goes through a controlled-field hook rather than many separateuseStatecalls.- Context — when a handful of components in a subtree need the same state and prop-drilling becomes noisy. Scope the provider to the feature module (
src/app/<domain>/contexts/), not the whole app. Remember context re-renders consumers on change, so keep context values small and stable. - Zustand / Jotai — only when the above are insufficient.
When a store is justified
Escalate to Zustand or Jotai when you have genuinely global, cross-cutting client state that many unrelated parts of the app read and write, where context would cause excessive re-renders or unwieldy provider nesting. Examples: an app-wide UI shell state shared across distant features, or fine-grained atoms that must update independently without re-rendering a large subtree.
Even then:
- Keep stores small and focused — one concern per store, scoped to where it's used.
- Never mirror server data into a store; let TanStack Query own it.
- Don't introduce a store "just in case." Add it when a concrete re-render or prop-drilling problem demands it.
Where state lives
- Component-local state stays in the component.
- Feature-scoped shared state lives in the feature module under
src/app/<domain>/contexts/(or a module-scoped store). - Truly global state is the rare exception, placed in shared
src/and documented.
The guiding principle: state should live as close as possible to where it's used, and no closer to the root than it has to be.