Spike: Global state management (React 19 + Vite)
Project acme-dashboard (React 19.1, Vite 6, TypeScript 5.6, pnpm). Today state lives in prop drilling and a few useContext calls; as the dashboard grew, re-renders and cross-screen coupling became friction. This spike compares three approaches for a global store, anchored to the real stack.
Objective and decision criteria
- Bundle: gzip weight added to first load (tight budget).
- Curve and DX: how fast the team ships without boilerplate.
- Mental model: single store vs. atoms; re-render predictability.
- Maturity: adoption, active maintenance, middleware ecosystem.
- Compatibility: React 19 + Vite + strict TS, no hacks.
Options analysed
Option 1 — Zustand
Store as a hook, outside the React tree; selectors control re-renders.
import { create } from "zustand";
type Store = { count: number; inc: () => void };
export const useStore = create<Store>((set) => ({
count: 0,
inc: () => set((s) => ({ count: s.count + 1 })),
}));
Pros: minimal API, no provider, lean selectors, great TS. Cons: selector discipline is on the team; unopinionated about architecture.
Option 2 — Jotai
State as composable atoms; only atom readers re-render.
import { atom, useAtom } from "jotai";
const countAtom = atom(0);
export const useCount = () => useAtom(countAtom);
Pros: natural granularity, excellent for derived state. Cons: many atoms scatter logic; steeper conceptual curve.
Option 3 — Redux Toolkit
Predictable store with slices, devtools and standard middleware.
Pros: strong conventions, mature devtools and RTK Query, scales for big teams. Cons: more boilerplate and weight; overkill for a mid-size dashboard.
Comparison
| Criterion | Zustand | Jotai | Redux Toolkit |
|---|---|---|---|
| Bundle (gzip) | ~1.1 kB | ~3.5 kB | ~13 kB (+react-redux) |
| Downloads/wk (npm) | ~5.5 M | ~1.6 M | ~4.8 M |
| Boilerplate | minimal | low | medium-high |
| Mental model | single store | atoms | store + slices |
| License | MIT | MIT | MIT |
Figures collected on 2026-07-12 (npm registry, npm downloads, bundlephobia). Verify before deciding — the ecosystem moves.
Recommendation
Adopt Zustand. For a mid-size React 19 SPA it strikes the best balance: negligible bundle, zero provider and a mental model the team absorbs in an afternoon. The current friction (prop drilling and re-renders) is solved with a store plus selectors, without changing the app's architecture.
Reconsider if: state is mostly derived and granular (Jotai shines there), or the team is already large and standardised on Redux with heavy RTK Query use — then Redux Toolkit's conventions pay for the extra weight.
Proof-of-concept plan
pnpm add zustandand createsrc/store/ui.tswith a real slice (e.g. dashboard filters). Timebox: 2h- Migrate ONE screen from prop drilling to the store, measuring re-renders with the React DevTools Profiler. Timebox: 3h
- Add persistence (
persist) only for what must survive reload. Timebox: 1h - Review: compare bundle before/after and validate selectors. Timebox: 1h
Risks and mitigation
| Risk | Mitigation |
|---|---|
| Poor selectors cause extra re-renders | Selector lint + review in the PoC PR |
| Store becomes a "global bucket" | Slices per domain; no server logic in the store |
| Server state in the wrong place | Keep data-fetching in React Query, store for UI only |
Spike acceptance criteria
- One screen migrated with re-renders measured before/after
- Bundle difference documented
- Slice convention written in the team README
- Decision recorded (this document) in the repository