From 9cb254ea13e894635af3492cc9d63b08b6ea9c89 Mon Sep 17 00:00:00 2001 From: null Date: Fri, 3 Jul 2026 20:08:28 -0500 Subject: [PATCH] refactor(bills): migrate BillsPage to React Query (R5.2) Reuses the shared useBills/useCategories caches (+ new useBillTemplates/ useDeletedBills), so bill mutations here now also refresh the Tracker/badge live via the shared ['bills'] key. Optimistic list edits (delete, reorder) write through queryClient.setQueryData; post-mutation load() calls became a refresh() that invalidates the 4 page queries. Co-Authored-By: Claude Opus 4.8 --- client/hooks/useQueries.js | 16 ++++++++++ client/pages/BillsPage.jsx | 64 ++++++++++++++++++-------------------- 2 files changed, 47 insertions(+), 33 deletions(-) diff --git a/client/hooks/useQueries.js b/client/hooks/useQueries.js index 4ab3d83..39dc23f 100644 --- a/client/hooks/useQueries.js +++ b/client/hooks/useQueries.js @@ -81,3 +81,19 @@ export function useAnalyticsSummary(params) { placeholderData: keepPreviousData, }); } + +export function useBillTemplates() { + return useQuery({ + queryKey: ['bill-templates'], + queryFn: () => api.billTemplates(), + staleTime: 1000 * 60 * 5, + }); +} + +export function useDeletedBills() { + return useQuery({ + queryKey: ['deleted-bills'], + queryFn: () => api.deletedBills().catch(() => []), // non-critical: never block the page + staleTime: 1000 * 60 * 5, + }); +} diff --git a/client/pages/BillsPage.jsx b/client/pages/BillsPage.jsx index 917a2d4..9f865a0 100644 --- a/client/pages/BillsPage.jsx +++ b/client/pages/BillsPage.jsx @@ -1,5 +1,7 @@ import React, { useEffect, useState, useCallback, useMemo, useRef } from 'react'; import { useSearchParams } from 'react-router-dom'; +import { useQueryClient } from '@tanstack/react-query'; +import { useBills, useCategories, useBillTemplates, useDeletedBills } from '@/hooks/useQueries'; import { Plus, ChevronRight, SlidersHorizontal, Search, Trash2 } from 'lucide-react'; import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; @@ -599,12 +601,28 @@ function HistoryVisibilityDialog({ bill, onClose, onSaved }) { // ── Bills Page ───────────────────────────────────────────────────────────── export default function BillsPage() { const [searchParams] = useSearchParams(); - const [bills, setBills] = useState([]); - const [categories, setCategories] = useState([]); - const [savedTemplates, setSavedTemplates] = useState([]); - const [deletedBills, setDeletedBills] = useState([]); + const queryClient = useQueryClient(); + // React Query is the source of truth (reuses the shared ['bills']/['categories'] + // caches, so bill mutations here also refresh the Tracker/badge). Optimistic + // list edits below write to the cache via setQueryData. + const { data: bills = [], isPending: billsPending } = useBills(); + const { data: categories = [] } = useCategories(); + const { data: savedTemplates = [] } = useBillTemplates(); + const { data: deletedBills = [] } = useDeletedBills(); + const loading = billsPending; + const setBills = useCallback( + (updater) => queryClient.setQueryData(['bills'], prev => ( + typeof updater === 'function' ? updater(prev || []) : updater + )), + [queryClient], + ); + const refresh = useCallback(() => Promise.all([ + queryClient.invalidateQueries({ queryKey: ['bills'] }), + queryClient.invalidateQueries({ queryKey: ['categories'] }), + queryClient.invalidateQueries({ queryKey: ['bill-templates'] }), + queryClient.invalidateQueries({ queryKey: ['deleted-bills'] }), + ]), [queryClient]); const [showDeleted, setShowDeleted] = useState(false); - const [loading, setLoading] = useState(true); const [showInactive, setShowInactive] = useState(false); const [search, setSearch] = useState(''); const [draggingId, setDraggingId] = useState(null); @@ -641,26 +659,6 @@ export default function BillsPage() { localStorage.setItem(BILLS_SORT_KEY, billsSort); }, [billsSort]); - const load = useCallback(async () => { - try { - const [billsRes, catRes, templateRes, deletedRes] = await Promise.all([ - api.allBills(), - api.categories(), - api.billTemplates(), - api.deletedBills().catch(() => []), // non-critical: never block the page - ]); - setBills(billsRes || []); - setCategories(catRes || []); - setSavedTemplates(templateRes || []); - setDeletedBills(deletedRes || []); - } catch (err) { - toast.error(err.message); - } finally { - setLoading(false); - } - }, []); - - useEffect(() => { load(); }, [load]); useEffect(() => { const querySearch = searchParams.get('search') || ''; @@ -745,7 +743,7 @@ export default function BillsPage() { if (bill.active && reason) payload.inactive_reason = reason; await api.updateBill(bill.id, payload); toast.success(bill.active ? 'Bill deactivated' : 'Bill activated'); - load(); + refresh(); } catch (err) { toast.error(err.message); } @@ -771,7 +769,7 @@ export default function BillsPage() { try { await api.restoreBill(bill.id); toast.success(`"${bill.name}" restored`); - load(); + refresh(); } catch (err) { toast.error(err.message || 'Failed to restore bill'); } @@ -780,7 +778,7 @@ export default function BillsPage() { }); setDeleteTarget(null); setDeleteConfirmed(false); - load(); + refresh(); } catch (err) { toast.error(err.message || 'Failed to delete bill'); } finally { @@ -792,7 +790,7 @@ export default function BillsPage() { try { await api.restoreBill(bill.id); toast.success(`"${bill.name}" restored`); - await load(); + await refresh(); } catch (err) { toast.error(err.message || 'Failed to restore bill'); } @@ -850,10 +848,10 @@ export default function BillsPage() { try { await api.reorderBills(reorderPayload(nextBills)); toast.success('Bill order saved'); - load(); + refresh(); } catch (err) { toast.error(err.message || 'Failed to save bill order'); - load(); + refresh(); } finally { setMovingBillId(null); } @@ -1155,7 +1153,7 @@ export default function BillsPage() { initialBill={modal.initialBill} categories={categories} onClose={() => setModal(null)} - onSave={load} + onSave={refresh} onDuplicate={handleDuplicateBill} /> )} @@ -1164,7 +1162,7 @@ export default function BillsPage() { setHistoryTarget(null)} - onSaved={load} + onSaved={refresh} /> )}