LabHub

Blog

State Management Renaissance 2025 — Zustand, Jotai, Valtio, TanStack Query, Signals, XState, RSC (S6 E10)

한국어English日本語

Prologue — 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:

This post maps each, when to reach for which, and the common anti-patterns.


1. The four categories of state

  1. Server state — data that the backend owns. User info, posts, products. It's cache, not state.
  2. URL state — state the URL already encodes. Current tab, sort order, search query.
  3. UI state — purely client-side presentation. Menu open, modal visible, theme.
  4. 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:

Minimal:

const { data, isLoading } = useQuery({
  queryKey: ['posts', postId],
  queryFn: () => fetch(`/api/posts/${postId}`).then(r => r.json()),
})

Competitors

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


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:

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

Don't use when

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


7. URL state — useSearchParams, nuqs

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:

Do not use it:


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

Zustand has persist middleware; Jotai has atomWithStorage; Valtio has proxyWithHistory.


11. Debugging and DevTools

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:


13. What to watch in 2026+

  1. React Compiler stable — auto-memoization changes what "performance" work looks like. useMemo/useCallback become rare.
  2. Signals as web standard — TC39 proposal progression means native primitives in browsers.
  3. RSC across frameworks — SolidStart, Remix (post-merger with React Router), Astro all adopting RSC-style boundaries.
  4. Actor model normalization — XState v5 actor pattern becoming more mainstream.
  5. Sync engines — Replicache, Zero (by Replicache team), Liveblocks blur the line between server and client state. "Local-first" rising.

12-item checklist

  1. Do you separate server, URL, client UI, and form state?
  2. Is server data in TanStack Query (or SWR/Apollo), not Redux?
  3. Is URL state reflected in the actual URL?
  4. Is Context used for DI, not frequently-changing state?
  5. Is there one default client store (Zustand or equivalent)?
  6. Are forms using RHF/TanStack Form + Zod?
  7. Are complex flows modeled with XState?
  8. Are you using RSC for server-fetched UI?
  9. Are DevTools integrated for every state layer?
  10. Is persistence using the right storage (local vs IDB vs cookie)?
  11. Are you avoiding useMemo/useCallback bloat (or using React Compiler)?
  12. Is the state shape documented for onboarding?

10 anti-patterns

  1. Server data in Redux/Zustand — stale bugs.
  2. URL state in local state — broken back button, unsharable links.
  3. Context for frequently-changing state — re-render storms.
  4. useState cascades in forms — use RHF instead.
  5. Global store for local modal state.
  6. State machine for a boolean toggle.
  7. Multiple Zustand stores overlapping.
  8. Fetching in useEffect — use TanStack Query.
  9. Persisting everything to localStorage "just in case."
  10. 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.

Comments

No comments yet.

Sign in to leave a comment