Saltar al contenido principal

ADR-003 — Frontend state: Zustand + TanStack Query

:::note Contenido en inglés Esta página del wiki se sincroniza desde la base de conocimiento en inglés y todavía no está traducida. :::

canonical · tech-lead · updated 2026-06-28 · source

  • Status: Accepted
  • Date: 2026-05-20
  • Owner: Tech Lead
  • Related issue: #6 (repo scaffold + ADRs)

Context

  • Desktop client has two clearly different kinds of state:
    1. UI state — sidebar collapsed, active view, modals, command palette open, settings, layout splits.
    2. Server state — issues, integrations, AI runs, notifications, user profile — owned by the API, fetched, cached, invalidated.
  • Mixing them in one store (Redux-style) blurs invalidation rules and bloats reducers.
  • Tauri v2 + React 19 + a desktop app that lives open 8h/day means cache freshness, focus refetching, and optimistic mutations are first-class concerns.

Decision

  • Use Zustand for client UI state.
    • One global store + slice pattern (useUiStore, useSettingsStore, useCommandPaletteStore).
    • Selectors with shallow to avoid over-renders.
    • Persist whitelist (e.g. theme, sidebar width) via zustand/middleware/persist.
  • Use TanStack Query (v5) for all server state.
    • useQuery for reads, useMutation for writes, invalidateQueries for cache surgery.
    • Query keys are typed via @tedos/shared constants (e.g. qk.issues.list(filters)).
    • SSE / WS push messages from API call queryClient.invalidateQueries or setQueryData.

Rule of thumb

Lives in…Examples
Zustandactive view, panel widths, modal open/close, theme, command-palette query string, draft form values not yet POSTed
TanStack Queryissues list, single issue detail, integrations status, AI run history, user profile, notifications
React local state (useState)transient component-only flags (hover, focus-within, animation in-progress)

Out of scope here: form library (separate ADR, likely React Hook Form), routing (Tauri windows + a small router TBD).

Consequences

Positive

  • Zustand is ~1kb, no provider, no boilerplate; reads like plain JS.
  • TanStack Query handles every hard cache problem (dedup, retries, focus refetch, pagination, optimistic updates) so we never write a reducer for that again.
  • Clean integration story: SSE/WS pushes mutate the query cache directly; UI panels keep using useQuery and just re-render.
  • Both libraries are framework-agnostic enough to survive a React Native or web port if we ever do one.

Negative / trade-offs

  • Two libraries to learn instead of one (mitigated: both have small surface areas).
  • Need a convention for query-key construction — codified in packages/shared/src/queryKeys.ts.
  • Devtools are separate (Zustand devtools + TanStack Query devtools) — fine in dev, both have React DevTools-compatible inspectors.

Follow-ups required

  • Frontend Engineer ships packages/shared/src/queryKeys.ts constants when first endpoint lands.
  • Designer specifies which UI state surfaces persist across launches (theme yes; modal state no).

Alternatives considered

OptionWhy not
Redux Toolkit + RTK QueryHeavier ergonomics, more files per slice, less mindshare in 2026. RTK Query is fine but TanStack Query has better DX and ecosystem.
Jotai + TanStack QueryJotai is great but atom-spaghetti is real on a 50-screen app; Zustand's slice + selector pattern scales better for our team size.
Plain React Context + useReducerRe-renders all consumers on every change; no devtools; we would rebuild Zustand poorly.
ValtioProxy-based magic is harder to reason about for new contributors; Zustand wins on explicitness.
TanStack Query alone (no client store)Forces hacky useQuery(['ui', 'sidebar']) patterns for things that aren't server state. Wrong abstraction.

References