087fbdd176
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>
124 lines
5.0 KiB
Markdown
124 lines
5.0 KiB
Markdown
# 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<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`**
|
|
```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() { ... }
|
|
```
|
|
|
|
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`
|