Files
budget/.plans/12-tanstack-query.md
T
Spencer Twaddle 087fbdd176 Split into Budget.Core / Budget.Infrastructure / Budget.Api projects
Budget.Core: entities, DTOs, enums, FrequencyCalculator (no EF/ASP.NET deps)
Budget.Infrastructure: AppDbContext, migrations, BudgetAuthorizationService
Budget.Api: controllers, middleware, Program.cs — references both projects

EF and Npgsql packages moved to Infrastructure; Api retains only JwtBearer,
HealthChecks, and EF.Design (needed for dotnet ef CLI). Dockerfile updated
to copy all three project directories before publishing. Migration namespaces
updated from Budget.Api.Data.* to Budget.Infrastructure.Data.* and model
type strings updated to Budget.Core.Models.* in the snapshot.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 16:30:31 -05:00

5.0 KiB

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:

const [budgets, setBudgets] = useState<BudgetDto[]>([]);
useEffect(() => {
  api.get<BudgetDto[]>('/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 <QueryClientProvider client={queryClient}> in main.tsx (inside <AuthProvider>).

Phase 2 — Create domain hook files

For each domain, create a hooks file following this pattern:

src/api/budgets.ts

export function useBudgets() {
  return useQuery({ queryKey: ['budgets'], queryFn: () => api.get<BudgetDto[]>('/api/budgets') });
}

export function useCreateBudget() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: (req: CreateBudgetRequest) => api.post<BudgetDto>('/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() { ... }
  1. Create src/api/budgets.ts — hooks for List, Create, Update, Delete.
  2. Create src/api/incomes.ts — hooks for List, Create, Update, Delete, Reorder. Query key: ['budgets', budgetId, 'incomes'].
  3. 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.
  4. Create src/api/shares.ts — hooks for List, Add, Update, Revoke. Query key: ['budgets', budgetId, 'shares'].
  5. Create src/api/summary.ts — hooks for Get and UpdateTaxRate. Query key: ['budgets', budgetId, 'summary'].

Phase 3 — Update pages to use hooks

  1. BudgetsPage.tsx — replace useEffect + useState with useBudgets() and useCreateBudget().
  2. IncomePage.tsx — replace with useIncomes(), useCreateIncome(), useUpdateIncome(), useDeleteIncome(), useReorderIncomes().
  3. OutgoPage.tsx — replace with outgo hooks + category/payment-source lookup hooks.
  4. SummaryPage.tsx — replace with useSummary() and useUpdateTaxRate().
  5. SettingsPage.tsx — replace with useShares(), useAddShare(), useUpdateShare(), useRevokeShare().

Phase 4 — Clean up

  1. Remove setTokenProvider from main.tsx if it is no longer wired there (it should now live only in TokenSync.tsx from plan #11).
  2. Remove unused useEffect / useState imports from pages.
  3. 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