Skip to main content

ADR-021 — TanStack Query + Table adoption

1. Context

noxys-console currently uses:

  • react-router-dom v7 for routing.
  • Manual fetch via src/lib/api.ts consumed in pages with useState + 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: 2 for transient errors, false for 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)

  1. DashboardPage — most-visited, currently has Array.isArray guards (PR #167 reminder).
  2. InteractionsPage — heavy refresh load, benefits most from focus-refetch.
  3. ShadowAIDiscoveryPage — pre-COMP refactor target.
  4. CompliancePage — pre-COMP-* sprints, simplifies before stacking complexity.
  5. AlertsPage + PoliciesPage + InventoryPage + UsersPage — second wave.
  6. 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

  1. InteractionsPage table — largest, highest UX win. Validates wrapper pattern.
  2. InventoryPage table — second, similar shape.
  3. UsersPage table — RBAC-aware columns.
  4. AlertsPage table.
  5. AuditPage table.
  6. Subscriptions internal staff table (per ADR-018 SUB-9) — net-new with TanStack Table from start.
  7. 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.
  • staleTime per 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

IDTitleRepoWindowEffortPriority
TANQ-1TanStack Query setup — provider, defaults, devtools, queries.ts scaffold; migrate DashboardPagenoxys-consoleW23-241dP2
TANQ-2Migrate Interactions + ShadowAI + Compliance pages to useQuery + invalidationnoxys-consoleW23-242dP2
TANQ-3Migrate Alerts + Policies + Inventory + Users + ServiceDetail to useQuery; mutations + optimistic where UX clearnoxys-consoleW25-261dP2
TANT-1TanStack Table — <DataTable> wrapper + Interactions migrationnoxys-consoleW27-282dP2
TANT-2Migrate Inventory + Users + Alerts + Audit tables; ensure Subscriptions + Reports history net-new on <DataTable>noxys-consoleW27-282dP2

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/useEffect and useQuery co-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)