State management
Each type of state has one designated place. Never mix them.
Matrix
Section titled “Matrix”| 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 |
TanStack Query
Section titled “TanStack Query”- Singleton
queryClientincore/query/query-client.ts(one instance for the whole lifecycle; the SSR-query router uses it too). - Defaults:
retrylimited 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()).
- Server state is never duplicated in
useState: the Query cache is the source.refetch()/setQueryDatato refresh. - A feature does not instantiate a
QueryClient(the singleton is enough) and declares its keys viacreateQueryKeys. - URL state is read/written through router search params — no manually
synchronized
useState.
Don’ts
Section titled “Don’ts”- ❌ “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
useStatewith a query (useEffect+setState) instead of using the Query cache.
