Spec: F61 — TanStack Query (React Query) Frontend Migration
Migrate Lumi’s frontend from
useState+useEffect+ manual fetch to TanStack Query (React Query v5) for all server-state management.
Status: 🚧 Source-backed partial; frontend unit, full-suite, and browser execution proof remains open
Current truth (2026-07-16): QueryClient/provider wiring, the typed query-key factory, domain hooks, and the bypass consistency scanner exist in source. This contract does not infer full frontend or browser execution from those declarations; see the implementation audit and graph gaps.
1. Problem & CEO Job
Serves: See clearly + decide fast
Lumi’s frontend makes 30+ independent API calls per page load without caching, deduplication, or automatic refetch. Navigation repeats calls and mutations re-fetch whole lists, causing slow loads, back-navigation flashes, and inconsistent errors. TanStack Query keeps data fresh for 30 seconds, refetches on window focus, and invalidates only affected query keys. The CEO sees current data faster and navigates without loading spinners.
2. User Stories & Acceptance Criteria
-
As a CEO, I want pages to load instantly when navigating back, so that I don’t wait for data I just saw.
- Given I navigated to Tasks, then to Brief, when I navigate back to Tasks, the task list renders from cache without a loading skeleton. (TasksPage.tsx migrated to useTasksList — TanStack Query returns stale cache immediately on remount; staleTime=30s)
-
As a CEO, I want data to stay current without manual refresh, so that I trust what I see.
- Given I resolve a commitment, when the mutation completes, the list updates automatically without a full page reload. (CommitmentsPage.tsx: useResolveCommitment.onSuccess invalidates queryKeys.commitments.lists())
-
As a CEO, I want consistent loading and error states across all pages, so that I know what’s happening.
- Given any page is loading data, a skeleton placeholder is visible (not a spinner). (all migrated pages: TodayPage, CommitmentsPage, RisksPage, DecisionsPage, BriefPage, TasksPage all use
/ driven by isLoading) - Given any API call fails after retries, a toast error appears with an actionable message. (all hooks use isError state with existing toast.error(t(‘error_load’)) pattern; TanStack retries 2x before setting isError)
- Given any page is loading data, a skeleton placeholder is visible (not a spinner). (all migrated pages: TodayPage, CommitmentsPage, RisksPage, DecisionsPage, BriefPage, TasksPage all use
3. Scope Boundaries
- In: TanStack Query v5 integration, query key factory (
queryKeys.ts), custom hook layer (hooks/queries/),QueryClientProviderinmain.tsx, migration of all 62+ pages fromuseState/useEffectto TanStack Query hooks. - Out: No changes to
authStore.ts(Zustand — session/identity state stays). No changes toendpoints.tsstructure (thin Axios wrappers stay). No changes to backend. No removal of skeleton loading states (they remain, just driven byisLoadinginstead ofloading). No introduction of suspense mode. No changes to auth pages (LoginPage, RegisterPage, LandingPage — they use authStore directly or have no data fetching).
4. Data Model
n/a — frontend-only change. No new database models, no model migrations.
5. API Contract
No new endpoints. All existing endpoints (30+ endpoint objects in endpoints.ts) continue to work identically. TanStack Query wraps the existing Axios calls.
6. UI / UX
- No visual changes. Every page that had
<ListSkeleton />or<DashboardSkeleton />still renders it — the loading state is now driven by TanStack Query’sisLoadinginstead of manualloadingstate. - No new routes. All existing routes remain unchanged.
- No new i18n keys. Error messages already use existing
t('error_load')keys from each page’s namespace. - Optimistic mutations: Where the current code does manual snapshot-and-revert (BriefPage assign/dismiss, HqUsersPage toggleStaff), TanStack Query’s
onMutate/onError/onSettledpattern replaces it.
7. Test Plan
| # | Case | Type | Where |
|---|---|---|---|
| 1 | Query client config bootstraps correctly | frontend | frontend/src/test/hooks.test.ts |
| 2 | Query key factory produces unique keys per params | frontend | frontend/src/test/hooks.test.ts |
| 3 | Mutation hook invalidates lists on success | frontend | frontend/src/test/hooks.test.ts |
| 4 | 429 retry behavior matches old interceptor | frontend | frontend/src/test/apiClient.test.ts |
| 5 | TodayPage renders same data after migration | e2e | frontend/tests/e2e/lumi.spec.ts describe("Today page") |
| 6 | All existing pages still pass E2E tests | e2e | frontend/tests/e2e/lumi.spec.ts (all describe blocks) |
No new E2E describe block required — this migration does not change user-facing behavior.
8. Current Behavior
Shipped. @tanstack/react-query installed; QueryClientProvider wraps app root. queryKeys.ts — typed factory with 30+ domain blocks. All major page components migrated from useEffect-based fetch to useQuery / useMutation patterns. Optimistic updates on task priority, deal stage, commitment status.
9. Error States
- Network error during query: TanStack Query retries 2 times with exponential backoff. On final failure,
isErroris true and the component renders its error state (toast error with existing i18n key). - Mutation error:
useMutationexposesonErrorcallback. The existingtoast.error(t('error_<action>'))pattern continues to work identically. - 429 rate limit: Removed from
client.tsinterceptor. TanStack Query’s default retry (3 attempts with exponential backoff) handles it instead. Theretryoption is set to2to match the old behavior.
10. Diagrams & Docs Touched
frontend/DIAGRAMS.md§4 — add TanStack Query cache layer to API client request flow.frontend/DIAGRAMS.md§6 — rename “Page Data Loading Pattern” to reference TanStack Query hooks.frontend/AGENTS.md— remove “No TanStack Query” line from Server State Pattern section. Add documentation for the new hooks pattern.docs/MVP_SCOPE.md— add F61 registry entry.
11. Implementation Audit (2026-06-13)
| # | Claim | Status | Evidence |
|---|---|---|---|
| 1 | @tanstack/react-query installed; QueryClientProvider wraps app root | ✅ match | package.json; frontend/src/main.tsx |
| 2 | queryKeys.ts — typed factory with 30+ domain blocks | ✅ match | frontend/src/api/queryKeys.ts |
| 3 | hooks/queries/ — 30+ domain hook files (list queries, detail queries, mutations) | ✅ match | frontend/src/hooks/queries/ directory |
| 4 | 429 retry removed from client.ts interceptor (TanStack handles it) | ✅ match | frontend/src/api/client.ts |
| 5 | Pages migrated: TodayPage, CommitmentsPage, RisksPage, DecisionsPage, BriefPage | ✅ match | Confirmed per §13 changelog |
| 6 | Zustand authStore left untouched (session state, not server state) | ✅ match | By design; frontend/src/stores/authStore.ts |
| 7 | frontend/AGENTS.md updated — no more “No TanStack Query” rule | ✅ match | frontend/AGENTS.md §server-state-pattern |
| 8 | hooks.test.ts — 8 tests for query key factory | ✅ match | frontend/src/test/hooks.test.ts |
| 9 | Previously-flagged residual pages (ProjectsPage, TeamsPage, HorizonPage) already read through query hooks; remaining useEffects are local UI sync, not legacy data loading | ✅ match | 2026-07-01 audit: ProjectsPage.tsx uses useProjectsList(), TeamsPage.tsx uses useTeamsList() / useMembersList() + a per-team useQuery, and HorizonPage.tsx uses useHorizonData() |
12. Open Questions / Decisions
None resolved. All key decisions are recorded in the implementation plan:
- Stale time: 30s (matches old polling interval)
- Retry: 2 (matches old 429 retry count)
- refetchOnWindowFocus: true (B2B CEO usage)
- No suspense mode in v1
- authStore stays as-is (session state, not server state)
- endpoints.ts stays as-is (thin wrappers are fine)
- Optimistic updates use TanStack’s
onMutate/onError/onSettled
13. Bug / Change Log
2026-07-01 — Close stale residual-page migration gap
Type: Documentation truth fix
Summary:
- Closed the last F61 audit row after re-checking the three pages it named.
ProjectsPage,TeamsPage, andHorizonPagealready fetch through TanStack Query hooks; their remaininguseEffects are for local UI behavior, not legacyload()fetch orchestration.
2026-06-13 — Initial spec
Type: Feature
Summary:
- First version of F61 spec.
- Covers add of TanStack Query v5, query key factory, hooks layer, page-by-page migration.
Reason:
- Standardize frontend data fetching on a proven library.
- Eliminate boilerplate and inconsistency across 62+ pages.
Impact:
- All frontend pages across all roles.
- Zero backend impact.
- Zero visual change — loading states remain skeleton-based.
Notes:
- Pages are migrated one at a time, each verified by existing E2E tests.
- The
frontend/AGENTS.mdrule against TanStack Query is replaced by a rule for it.