ADR-021 — TanStack Query + Table adoption
1. Context
noxys-console currently uses:
react-router-dom v7for routing.- Manual
fetchviasrc/lib/api.tsconsumed in pages withuseState+useEffect+ ad-hoc loading/error state. - Native HTML
<table>+ shadcn primitives for tables — sorting/filtering/pagination handled by hand per page.
Symptoms that justify a server-state library:
- Recurring "X is not iterable" defensive-guard bugs (PRs #167-#172 + cursor refactor #173) traced back to manual handling of pagination shapes + over-eager cache assumptions.
- Multiple widgets fetching the same endpoint (Dashboard tile + sidebar + breadcrumb count) → no dedup, redundant calls.
- No background refetch on focus/online — operators get stale data after long sessions.
- Optimistic updates impossible without rebuilding state plumbing per mutation (policy create, user invite, etc.).
- Tables across Interactions, Inventory, Users, Alerts, Audit, Subscriptions, Reports history, Lineage timeline — copy-pasted sort/filter/paginate logic, no virtualization.
TanStack family solves both classes of problem. We are not adopting the entire family — only what gives clear ROI without high migration cost.
2. Decision summary
Adopt:
- TanStack Query v5 — server-state caching, dedup, refetch-on-focus, optimistic updates, devtools.
- TanStack Table v8 — headless table engine (sorting, filtering, pagination, column resize, virtualization-ready).
Skip:
- TanStack Router — react-router-dom v7 is already in place; migration cost > marginal benefit (type-safe routes nice-to-have, not need-to-have).
- TanStack Form — no current pain point; if we add it later, swap is local per form.
- TanStack Start — Vite + react-router stack is fine; no SSR/meta-framework need.
Defer (not skip):
- TanStack Virtual — adopt only on a per-table basis when row count or profiling data justifies (likely Interactions list at scale).
3. Scope of TanStack Query adoption
Backward-compatible wrap
Existing fetchers in src/lib/api.ts stay. We add a thin useQueryFetcher<T>(fn, key) helper that wraps any fetcher into a React Query hook. Pages migrate page-by-page; coexistence with manual useState/useEffect is supported during the transition.
// src/lib/queries.ts (new)
import { useQuery, useMutation, queryOptions } from '@tanstack/react-query';
import * as api from './api';
export const interactionsQuery = (params: ListParams) => queryOptions({
queryKey: ['interactions', params],
queryFn: () => api.fetchInteractions(params),
staleTime: 30_000,
});
export const useInteractions = (params: ListParams) =>
useQuery(interactionsQuery(params));
Pages then:
// before
const [data, setData] = useState();
const [loading, setLoading] = useState(true);
useEffect(() => { fetchInteractions(params).then(setData).finally(() => setLoading(false)); }, [params]);
// after
const { data, isLoading } = useInteractions(params);
Configuration
Single QueryClientProvider at src/main.tsx. Global defaults:
staleTime: 30_000(30s) — most security data refreshes seconds-scale, not real-time.gcTime: 5 * 60_000(5min cache after unmount).retry: 2for transient errors,falsefor 4xx.refetchOnWindowFocus: true— operators leaving tab and returning expect fresh data.refetchOnReconnect: true.
Devtools (@tanstack/react-query-devtools) included in dev build only. Excluded from prod bundle via Vite define.
Mutations + invalidation
Each mutation uses useMutation + queryClient.invalidateQueries. Convention: invalidation key matches the listing query key prefix (e.g. mutation createPolicy invalidates ['policies']). Optimistic updates introduced page-by-page where UX benefit clear.
Migration order (priority by impact)
- DashboardPage — most-visited, currently has Array.isArray guards (PR #167 reminder).
- InteractionsPage — heavy refresh load, benefits most from focus-refetch.
- ShadowAIDiscoveryPage — pre-COMP refactor target.
- CompliancePage — pre-COMP-* sprints, simplifies before stacking complexity.
- AlertsPage + PoliciesPage + InventoryPage + UsersPage — second wave.
- ServiceDetailPage + smaller views — third wave.
4. Scope of TanStack Table adoption
Wrap pattern
Single shared <DataTable> wrapper in src/components/data-table/ built on TanStack Table primitives + shadcn <Table> styling. Each page consumes the wrapper passing column defs.
Features delivered out-of-the-box:
- Multi-column sort
- Per-column filter (text, select, date-range)
- Pagination (server-driven via Query + cursor)
- Column visibility toggle
- Column reorder + resize (persist user preference in localStorage per table)
- Row selection (checkboxes) with bulk actions slot
- Sub-rows expand/collapse (foundational for Lineage timeline + nested audit views)
Migration order
- InteractionsPage table — largest, highest UX win. Validates wrapper pattern.
- InventoryPage table — second, similar shape.
- UsersPage table — RBAC-aware columns.
- AlertsPage table.
- AuditPage table.
- Subscriptions internal staff table (per ADR-018 SUB-9) — net-new with TanStack Table from start.
- Reports history table (per ADR-016 COMP-6) — net-new with TanStack Table from start.
Page-level virtualization added later only when profiling shows scroll lag.
5. Tier / RBAC implications
- TanStack Query keys must include
tenant_id(from auth context) implicitly via the existing fetcher signature — no leak between tenants. staleTimeper tier: Free = 60s (reduce backend load on free pool), Pro+ = 30s (snappier UX).- Devtools NEVER in prod bundle — security risk + payload size.
6. Performance targets
- Initial Dashboard render with 8 panels: ≤ 1.5s P75 (current: 2.2s with manual fetches racing).
- Interactions table 1000 rows: ≤ 200ms render (current: ~600ms).
- Memory baseline: Query cache <30MB at steady state.
7. Sprint plan — 5 sprints, 8 days
| ID | Title | Repo | Window | Effort | Priority |
|---|---|---|---|---|---|
| TANQ-1 | TanStack Query setup — provider, defaults, devtools, queries.ts scaffold; migrate DashboardPage | noxys-console | W23-24 | 1d | P2 |
| TANQ-2 | Migrate Interactions + ShadowAI + Compliance pages to useQuery + invalidation | noxys-console | W23-24 | 2d | P2 |
| TANQ-3 | Migrate Alerts + Policies + Inventory + Users + ServiceDetail to useQuery; mutations + optimistic where UX clear | noxys-console | W25-26 | 1d | P2 |
| TANT-1 | TanStack Table — <DataTable> wrapper + Interactions migration | noxys-console | W27-28 | 2d | P2 |
| TANT-2 | Migrate Inventory + Users + Alerts + Audit tables; ensure Subscriptions + Reports history net-new on <DataTable> | noxys-console | W27-28 | 2d | P2 |
Effort fits between higher-priority sprints (EXT-UX, COMP-, AI-PLACE-, LINEAGE-*) — designed to absorb developer interstitial time in W23-28.
8. Backward compatibility + rollback
- Coexistence guaranteed: pages migrate one at a time. Manual
useState/useEffectanduseQueryco-exist in the codebase. - Rollback per page: revert PR — wrapper helpers stay, page reverts to manual.
- No DB or API contract change. Pure frontend refactor.
9. Dependencies added
{
"@tanstack/react-query": "^5",
"@tanstack/react-query-devtools": "^5",
"@tanstack/react-table": "^8"
}
Bundle impact estimate: +25KB gzipped (Query + Table + light wrapper). Acceptable for a ~200KB-target bundle (web rules).
10. Non-goals
- Do not adopt TanStack Router (no migration from react-router-dom v7).
- Do not adopt TanStack Form (no current pain point).
- Do not adopt TanStack Start (no SSR / meta-framework need).
- Do not refactor api.ts fetcher signatures (wrap, don't replace — keeps surface stable for Storybook + tests).
11. Influence on other sprints
- LINEAGE-3..6 (ADR-020): use TanStack Query from day one (no manual fetch). Lineage trace polling V2 → SSE-aware Query subscription.
- COMP-6 (ADR-016 Compliance Reports page): net-new — adopt TanStack Query + Table from day one.
- SUB-8/9 (ADR-018 Subscription pages): same — adopt from day one.
- EXT-UX-8 (ADR-015 Managed deployment console dashboard): same.
- All console pages built after W23-24 should be Query-first; pre-W23-24 pages migrate per §3 schedule.
12. Open questions
- react-query persistor: persist Query cache to localStorage for instant load? V1 = no; reassess if cold-start UX suffers.
- SSR future: if we ever ship a SSG marketing site that fetches console-grade data (unlikely), Query has SSR helpers. Not relevant now.
- Table virtualization: defer until profiling shows pain. Likely Interactions at 5k+ rows.
13. Refs
- noxys-console CLAUDE.md (current stack inventory)
- PR #173 (cursor pagination refactor — symptoms motivating this ADR)
- PRs #167-#172 (defensive guard bugs — motivating Query adoption)
- ADR-020 Interaction lineage visualization (consumes TanStack Query)
- Future ADR-027 CRM choice (renumbered through ADR-019..ADR-026 — all taken; ADR-026 = API/OpenAPI policy; CRM lands on ADR-027 when picked)
- ADR-022 UsersPage V2 spec (consumes DataTable wrapper for users table; sources entitlements/cost from backend rollups)
- ADR-024 Authentication & MFA strategy (TOTP/WebAuthn/backup codes encrypted via KMS pattern; tenant token reveal + tenant-wide MFA enforcement)
- ADR-026 API completeness & OpenAPI policy (every endpoint defined in this ADR must have OpenAPI 3.1 spec entry; backfill obligation in §9)