✍️ 필사 모드: State Management Renaissance 2025 — Zustand, Jotai, Valtio, TanStack Query, Signals, XState, RSC (S6 E10)
EnglishPrologue — the state management renaissance
"Which state management library?" was the 2018 interview question. By 2021 everyone hated Redux boilerplate. By 2024 nobody defaults to Redux Toolkit anymore — the question fractured into which state, where?
Today state lives in several distinct homes:
- Server state → TanStack Query / SWR / Apollo.
- URL state → router params, search params.
- Server Components → state never reaches the client.
- Client UI state → Zustand / Jotai / Valtio / React Context.
- Form state → React Hook Form / TanStack Form.
- State machines → XState / Robot / tiny-fsm.
- Derived / reactive → Signals (Preact, Solid, Angular; React proposal).
This post maps each, when to reach for which, and the common anti-patterns.
1. The four categories of state
- Server state — data that the backend owns. User info, posts, products. It's cache, not state.
- URL state — state the URL already encodes. Current tab, sort order, search query.
- UI state — purely client-side presentation. Menu open, modal visible, theme.
- Form state — transient editing state before submit.
Most bugs come from putting state in the wrong category. Server data in Redux = stale bugs. URL state in local state = broken back button.
2. Server state — TanStack Query wins
TanStack Query (née React Query) ate the server-state category. Why:
- Caching, deduplication, background refetch, stale-while-revalidate — free.
- Mutation + optimistic updates.
- Infinite queries, pagination helpers.
- Works with fetch, axios, GraphQL, tRPC, anything returning a Promise.
Minimal:
const { data, isLoading } = useQuery({
queryKey: ['posts', postId],
queryFn: () => fetch(`/api/posts/${postId}`).then(r => r.json()),
})
Competitors
- SWR — lighter, similar ideas. Good pick for simple apps.
- Apollo Client — if you're on GraphQL anyway.
- RTK Query — part of Redux Toolkit, for teams already there.
Rule: never put server data in Redux/Zustand. Use TanStack Query.
3. Client UI state — Zustand, Jotai, Valtio
Zustand
Global store, minimal API, no boilerplate.
import { create } from 'zustand'
const useStore = create((set) => ({
count: 0,
increment: () => set((s) => ({ count: s.count + 1 })),
}))
Strengths: small (~1kb), works anywhere, Redux DevTools integration, immer middleware. Use when: global UI state (theme, modal, sidebar). Simple apps.
Jotai
Atom-based. Each piece of state is an atom; components subscribe to atoms.
import { atom, useAtom } from 'jotai'
const countAtom = atom(0)
const [count, setCount] = useAtom(countAtom)
Strengths: fine-grained reactivity, great for complex derived state, middleware rich. Use when: lots of small independent pieces, derived atoms, persist/sync needs.
Valtio
Proxy-based. Mutate the state, components re-render.
import { proxy, useSnapshot } from 'valtio'
const state = proxy({ count: 0 })
state.count++ // triggers re-render in subscribed components
Strengths: most natural API for imperative mutation. Good for canvas/whiteboard apps. Use when: complex nested state with many mutation points.
Which to pick
- 90% of apps → Zustand.
- Complex derived state → Jotai.
- Imperative mutation-heavy (editors, whiteboards) → Valtio.
- Enterprise consistency → Redux Toolkit (still the safest "team onboarding" pick).
4. Signals — the reactive revolution
Signals are fine-grained reactive primitives popularized by Solid, adopted by Preact, Svelte 5 (runes), Angular, Vue (refs), and a standards-track TC39 proposal.
// Preact Signals
import { signal, computed } from '@preact/signals'
const count = signal(0)
const doubled = computed(() => count.value * 2)
// Update: count.value++
Why signals matter:
- No re-render cascade — only the DOM nodes that read the signal update.
- No dependency array — the dependency graph is implicit.
- Composable —
computedgives you derived state without React'suseMemogymnastics.
React status (2026): the TC39 Signals proposal is at Stage 1. React itself hasn't adopted signals; the React team prefers compiler-based auto-memoization (React Compiler, RC in 2025). For now, signals in React happen via libraries (@preact/signals-react, @preact/signals-react-runtime, or via Jotai for signal-like atoms).
5. State machines — XState
XState models state as an actor with explicit states, transitions, and guards.
import { createMachine } from 'xstate'
const fetchMachine = createMachine({
id: 'fetch',
initial: 'idle',
states: {
idle: { on: { FETCH: 'loading' } },
loading: { on: { SUCCESS: 'done', ERROR: 'error' } },
done: {},
error: { on: { RETRY: 'loading' } },
},
})
Use when
- Complex UI flows (multi-step forms, wizards, onboarding).
- Status that has many states (not just "isLoading / error / data").
- You need visual state diagrams for product managers.
Don't use when
- Simple on/off flags.
- State managed entirely by TanStack Query.
XState v5 (2024) focused on TypeScript ergonomics, actor model, and tiny bundle. Robot and @xstate/fsm are smaller alternatives.
6. Form state — React Hook Form / TanStack Form
React Hook Form (RHF)
The 2020–2024 default. Uncontrolled inputs, Zod integration via resolvers, great performance.
TanStack Form (2024+)
Framework-agnostic, headless. Richer for complex forms, better TS inference.
When to reach for either
- Any form longer than a single input. Don't build form state from
useState. - Use Zod or Valibot for schema + validation.
7. URL state — useSearchParams, nuqs
- Next.js
useSearchParams+ server component reads. nuqs— typed search params, syncs with URL on change.@tanstack/router— typed router with first-class search params.
Rule: if it changes the view and could be shared via link, it should be URL state. Tabs, filters, sort order, pagination.
8. React Context — the misunderstood tool
Context is a dependency injection mechanism, not a state library. Use it:
- For infrequently-changing dependencies (theme, auth user, i18n).
- To pass things down without prop drilling.
Do not use it:
- For frequently-changing state (every change re-renders all consumers).
- As a replacement for Zustand/Jotai.
9. React Server Components — state that isn't
RSC eliminates client state for a huge class of problems. If the data comes from the server and never changes on the client, it doesn't need state management at all.
Rule: ask "could this be in an RSC?" before adding anything to a store. The best state is no state.
10. Persistence — localStorage, IndexedDB, cookies
- localStorage — sync, small, for theme/preferences.
- IndexedDB (via Dexie or
idb-keyval) — large, async, for offline data. - Cookies — for server-readable state (auth, preferences SSR needs).
Zustand has persist middleware; Jotai has atomWithStorage; Valtio has proxyWithHistory.
11. Debugging and DevTools
- Redux DevTools integrates with Zustand, Jotai, Valtio — time travel, action log.
- TanStack Query DevTools — cache inspector, per-query status.
- XState Inspector / @xstate/inspect — live state machine viewer.
- React DevTools Profiler — render tracking.
Rule: if your state solution doesn't give you time travel or a state tree view, you'll pay for it later.
12. The 2026 stack
A pragmatic default for a new React app:
- Server state: TanStack Query.
- Client UI state: Zustand (one store).
- Forms: React Hook Form + Zod.
- URL state: nuqs (or router built-ins).
- State machines: XState only when complexity warrants.
- Derived reactivity: React Compiler (2025 RC) or Jotai atoms.
- RSC for server-only data.
13. What to watch in 2026+
- React Compiler stable — auto-memoization changes what "performance" work looks like.
useMemo/useCallbackbecome rare. - Signals as web standard — TC39 proposal progression means native primitives in browsers.
- RSC across frameworks — SolidStart, Remix (post-merger with React Router), Astro all adopting RSC-style boundaries.
- Actor model normalization — XState v5 actor pattern becoming more mainstream.
- Sync engines — Replicache, Zero (by Replicache team), Liveblocks blur the line between server and client state. "Local-first" rising.
12-item checklist
- Do you separate server, URL, client UI, and form state?
- Is server data in TanStack Query (or SWR/Apollo), not Redux?
- Is URL state reflected in the actual URL?
- Is Context used for DI, not frequently-changing state?
- Is there one default client store (Zustand or equivalent)?
- Are forms using RHF/TanStack Form + Zod?
- Are complex flows modeled with XState?
- Are you using RSC for server-fetched UI?
- Are DevTools integrated for every state layer?
- Is persistence using the right storage (local vs IDB vs cookie)?
- Are you avoiding
useMemo/useCallbackbloat (or using React Compiler)? - Is the state shape documented for onboarding?
10 anti-patterns
- Server data in Redux/Zustand — stale bugs.
- URL state in local state — broken back button, unsharable links.
- Context for frequently-changing state — re-render storms.
useStatecascades in forms — use RHF instead.- Global store for local modal state.
- State machine for a boolean toggle.
- Multiple Zustand stores overlapping.
- Fetching in
useEffect— use TanStack Query. - Persisting everything to localStorage "just in case."
- Avoiding RSC because "we've always used client state."
Next episode
Season 6 Episode 11: Frontend Testing 2025 — Vitest, Jest, Bun test, Testing Library, Playwright, Storybook, MSW, visual regression, AI-generated tests. What to write, in what proportions, and how to keep CI green.
— End of State Management Renaissance.
현재 단락 (1/142)
"Which state management library?" was the 2018 interview question. By 2021 everyone hated Redux boil...