Skip to main content

ADR-0009: State split — Redux for auth, React Query for server state, Zustand for islands

Status: Accepted (retroactive) Date: 2026-07-24

Context​

A single state library forced into every role serves none of them well:

  • Auth state must be readable and writable outside React — the axios interceptors attach tokens, run silent refresh, and dispatch logout from plain modules with no component tree in sight.
  • Server data (employees, payroll runs, grids) needs caching, invalidation, and background refetch — hand-building that in Redux is the classic mistake the ecosystem moved away from.
  • Small UI islands (sidebar collapse, breadcrumbs) and the customer portal's separate session need cheap local stores without Redux ceremony.

Decision​

Three tools, each scoped to exactly one job (all wired in main.tsx):

  • Redux Toolkit — the auth slice only. redux/auth.ts holds token/refreshToken/user/activeRole, persisted to localStorage. redux/store.ts registers just this one reducer. The payoff is in api/client.ts: store.getState().auth and store.dispatch(...) from a non-React module power token injection and the single-flight silent refresh (ADR-0003).
  • TanStack React Query — all server state. Every API read/write flows through hooks in hooks/api/ wrapping thin axios modules; components never cache server data themselves (architecture.md §2).
  • Zustand — portal and UI islands. store/ holds portalAuthStore, portalCartStore, portalWishlistStore, sidebarStore, breadcrumbStore. Notably the customer portal's auth is deliberately Zustand, not Redux — it is a separate auth system (24-hour JWT, no refresh rotation) and keeping it out of the ERP auth slice keeps the two from being confused.

Consequences​

Positive

  • Interceptors get first-class store access without hacks (no context smuggling, no event buses), which is the load-bearing requirement.
  • React Query eliminates a whole category of hand-rolled cache/loading/error state; grid pages share one idiom.
  • Each library is used only where it is the best tool, and each usage is small enough to reason about.

Negative

  • Three state idioms to learn. A newcomer must know when to reach for a Redux selector, a query hook, or a Zustand store — and the "rule" lives in docs and review, not in tooling. Misplaced state (server data in Zustand, UI state in Redux) compiles fine.
  • Two auth code paths by design: ERP auth (Redux + rotating refresh) and portal auth (Zustand, no refresh) duplicate concepts — login, logout, persistence — and every security change must remember both. The e2e helpers already special-case the persisted Redux shape (e2e/helpers/serverGrid.ts).
  • Redux Toolkit is a heavyweight dependency for one slice; it stays only because of the outside-React access pattern.

References​