Skip to content
Published on

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

Authors

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:

  • 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

  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:

  • 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.
  • Composablecomputed gives you derived state without React's useMemo gymnastics.

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+

  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.