# Plan: Add TanStack React Query ## Goal Replace the ad-hoc `useEffect` + `useState` fetch pattern with `@tanstack/react-query`. All data fetching moves into typed `useQuery` / `useMutation` hooks. A global `MutationCache` fires toasts on success and error so pages don't have to wire that up individually. **Prerequisite:** Plan #11 (react-oidc-context) must be complete, as `TokenSync` replaces the `setTokenProvider` wiring that pages currently rely on. ## Current state Each page manages its own loading/error state with `useState` and fetches in `useEffect`: ```ts const [budgets, setBudgets] = useState([]); useEffect(() => { api.get('/api/budgets').then(setBudgets); }, []); ``` Mutations are plain `async` calls with no error handling or cache invalidation. ## Target state ``` src/api/ client.ts — unchanged fetch wrapper queryClient.ts — QueryClient with MutationCache for global toasts budgets.ts — useQuery/useMutation hooks for budgets incomes.ts — hooks for incomes outgos.ts — hooks for outgos shares.ts — hooks for shares summary.ts — hooks for summary ``` Pages become thin: call a hook, render data — no fetch logic inline. ## Steps ### Phase 1 — Install and configure QueryClient 1. `npm install @tanstack/react-query` in `src/Budget.Client/`. 2. Create `src/Budget.Client/src/api/queryClient.ts`: - Create a `QueryClient` with a `MutationCache` that reads `meta.successMessage` and `meta.errorMessage` from each mutation and fires the existing `toast` utility on `onSuccess` / `onError`. - Export the `queryClient` singleton. 3. Wrap the app in `` in `main.tsx` (inside ``). ### Phase 2 — Create domain hook files For each domain, create a hooks file following this pattern: **`src/api/budgets.ts`** ```ts export function useBudgets() { return useQuery({ queryKey: ['budgets'], queryFn: () => api.get('/api/budgets') }); } export function useCreateBudget() { const qc = useQueryClient(); return useMutation({ mutationFn: (req: CreateBudgetRequest) => api.post('/api/budgets', req), onSuccess: () => qc.invalidateQueries({ queryKey: ['budgets'] }), meta: { successMessage: 'Budget created', errorMessage: 'Failed to create budget' }, }); } export function useUpdateBudget(id: string) { ... } export function useDeleteBudget() { ... } ``` 4. Create `src/api/budgets.ts` — hooks for List, Create, Update, Delete. 5. Create `src/api/incomes.ts` — hooks for List, Create, Update, Delete, Reorder. Query key: `['budgets', budgetId, 'incomes']`. 6. Create `src/api/outgos.ts` — hooks for List, Create, Update, Delete, Reorder. Query key: `['budgets', budgetId, 'outgos']`. Also hooks for `categories` and `payment-sources` lookups. 7. Create `src/api/shares.ts` — hooks for List, Add, Update, Revoke. Query key: `['budgets', budgetId, 'shares']`. 8. Create `src/api/summary.ts` — hooks for Get and UpdateTaxRate. Query key: `['budgets', budgetId, 'summary']`. ### Phase 3 — Update pages to use hooks 9. **`BudgetsPage.tsx`** — replace `useEffect` + `useState` with `useBudgets()` and `useCreateBudget()`. 10. **`IncomePage.tsx`** — replace with `useIncomes()`, `useCreateIncome()`, `useUpdateIncome()`, `useDeleteIncome()`, `useReorderIncomes()`. 11. **`OutgoPage.tsx`** — replace with outgo hooks + category/payment-source lookup hooks. 12. **`SummaryPage.tsx`** — replace with `useSummary()` and `useUpdateTaxRate()`. 13. **`SettingsPage.tsx`** — replace with `useShares()`, `useAddShare()`, `useUpdateShare()`, `useRevokeShare()`. ### Phase 4 — Clean up 14. Remove `setTokenProvider` from `main.tsx` if it is no longer wired there (it should now live only in `TokenSync.tsx` from plan #11). 15. Remove unused `useEffect` / `useState` imports from pages. 16. `npm run build` — zero TypeScript errors. ## Key decisions - `api/client.ts` is not changed — hooks wrap it, they don't replace it. - Cache invalidation strategy: mutations invalidate the narrowest relevant query key (e.g., a deleted income invalidates `['budgets', id, 'incomes']`, not all budgets). - No optimistic updates in this plan — add separately if needed. - `meta.successMessage` and `meta.errorMessage` are typed by augmenting the `@tanstack/react-query` module's `Register` interface so TypeScript validates them. ## Files affected - `package.json` - `src/Budget.Client/src/main.tsx` - New: `src/Budget.Client/src/api/queryClient.ts` - New: `src/Budget.Client/src/api/budgets.ts` - New: `src/Budget.Client/src/api/incomes.ts` - New: `src/Budget.Client/src/api/outgos.ts` - New: `src/Budget.Client/src/api/shares.ts` - New: `src/Budget.Client/src/api/summary.ts` - `src/Budget.Client/src/pages/BudgetsPage.tsx` - `src/Budget.Client/src/pages/IncomePage.tsx` - `src/Budget.Client/src/pages/OutgoPage.tsx` - `src/Budget.Client/src/pages/SummaryPage.tsx` - `src/Budget.Client/src/pages/SettingsPage.tsx`