Skip to content

State management

Each type of state has one designated place. Never mix them.

State type Where Example
Server state (API data) TanStack Query (core/query) useHealthQuery(), feature queries
Session / auth state core/auth (useSyncExternalStore snapshot) useCurrentUser(), useCan()
URL state (params, query, hash) TanStack Router (search params, path params) shareable list filter, pagination
Form state TanStack Form (@tanstack/react-form) login, feature forms
Transient local component state useState panel open/closed, non-persisted field
  • Singleton queryClient in core/query/query-client.ts (one instance for the whole lifecycle; the SSR-query router uses it too).
  • Defaults: retry limited to network errors (the API wrapper already handles the 401 refresh), refetchOnWindowFocus: false, staleTime: 30 s, mutations without automatic retry.
  • Query keys via createQueryKeys(scope):
const contratsKeys = createQueryKeys('residence.contrats')
queryClient.invalidateQueries({ queryKey: contratsKeys.all })
queryClient.setQueryData(contratsKeys.detail(id), data)
  • Targeted invalidation after a mutation (never a global invalidateQueries()).
  1. Server state is never duplicated in useState: the Query cache is the source. refetch()/setQueryData to refresh.
  2. A feature does not instantiate a QueryClient (the singleton is enough) and declares its keys via createQueryKeys.
  3. URL state is read/written through router search params — no manually synchronized useState.
  • ❌ “Just in case” global state (catch-all context) without justification.
  • ❌ Read the same query multiple times with different options (inconsistent cache) — centralize in a feature hook.
  • ❌ Synchronize useState with a query (useEffect + setState) instead of using the Query cache.