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