From a37697d492c4bd67aa7e142e3c76c0d27738b6b4 Mon Sep 17 00:00:00 2001 From: null Date: Fri, 3 Jul 2026 20:14:31 -0500 Subject: [PATCH] refactor(summary): migrate SummaryPage to React Query (R5.4) useSummary(year, month) with keepPreviousData for smooth month nav. The editable form fields (starting amounts, income) that loadSummary used to seed inline are now seeded from the query result via a data-synced effect; refetchOnWindowFocus is off so a background refetch can't reset a mid-edit. loadSummary is now an invalidate wrapper (retry + post-mutation reconciliation), and the optimistic expenses reorder writes through setQueryData. Co-Authored-By: Claude Opus 4.8 --- client/hooks/useQueries.js | 12 ++++++++ client/pages/SummaryPage.jsx | 60 ++++++++++++++++-------------------- 2 files changed, 39 insertions(+), 33 deletions(-) diff --git a/client/hooks/useQueries.js b/client/hooks/useQueries.js index 8e3818a..a47ba55 100644 --- a/client/hooks/useQueries.js +++ b/client/hooks/useQueries.js @@ -98,6 +98,18 @@ export function useDeletedBills() { }); } +export function useSummary(year, month) { + return useQuery({ + queryKey: ['summary', year, month], + queryFn: () => api.summary(year, month), + staleTime: 1000 * 60 * 2, + placeholderData: keepPreviousData, + // Editable form fields (starting amounts, income) are seeded from this + // result via an effect; don't let a focus refetch reset a mid-edit. + refetchOnWindowFocus: false, + }); +} + export function useSubscriptions() { return useQuery({ queryKey: ['subscriptions'], diff --git a/client/pages/SummaryPage.jsx b/client/pages/SummaryPage.jsx index fa0be77..d1d5e37 100644 --- a/client/pages/SummaryPage.jsx +++ b/client/pages/SummaryPage.jsx @@ -1,4 +1,6 @@ -import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { useQueryClient } from '@tanstack/react-query'; +import { useSummary } from '@/hooks/useQueries'; import { toast } from 'sonner'; import { ArrowDown, @@ -235,10 +237,12 @@ function ExpenseRow({ expense, moveControls, dragProps }) { export default function SummaryPage() { const [selected, setSelected] = useState(selectedFromToday); - const [data, setData] = useState(null); - const [loading, setLoading] = useState(true); + const queryClient = useQueryClient(); + const { data = null, isPending: loading, error: queryError } = useSummary(selected.year, selected.month); + const setData = useCallback((u) => queryClient.setQueryData(['summary', selected.year, selected.month], + prev => (typeof u === 'function' ? u(prev) : u)), [queryClient, selected.year, selected.month]); const [saving, setSaving] = useState(false); - const [error, setError] = useState(''); + const error = queryError ? (queryError.message || 'Summary could not be loaded.') : ''; const [startingFirst, setStartingFirst] = useState('0'); const [startingFifteenth, setStartingFifteenth] = useState('0'); const [startingOther, setStartingOther] = useState('0'); @@ -250,37 +254,27 @@ export default function SummaryPage() { const [dropTargetId, setDropTargetId] = useState(null); const [movingBillId, setMovingBillId] = useState(null); - const requestSeq = useRef(0); - const loadSummary = useCallback(async () => { - // Ignore out-of-order responses: only the newest month load updates state. - const seq = ++requestSeq.current; - setLoading(true); - setError(''); - try { - const result = await api.summary(selected.year, selected.month); - if (seq !== requestSeq.current) return; // superseded by a newer request - setData(result); - setStartingFirst(String(result.starting_amounts?.first_amount ?? 0)); - setStartingFifteenth(String(result.starting_amounts?.fifteenth_amount ?? 0)); - setStartingOther(String(result.starting_amounts?.other_amount ?? 0)); - setEditingStarting(false); - setIncomeAmount(String(result.income?.amount ?? 0)); - setIncomeLabel(result.income?.label || 'Salary'); - setEditingIncome(false); - setDraggingId(null); - setDropTargetId(null); - setMovingBillId(null); - } catch (err) { - if (seq === requestSeq.current) setError(err.message || 'Summary could not be loaded.'); - toast.error(err.message || 'Summary could not be loaded.'); - } finally { - if (seq === requestSeq.current) setLoading(false); - } - }, [selected.month, selected.year]); + const loadSummary = useCallback( + () => queryClient.invalidateQueries({ queryKey: ['summary', selected.year, selected.month] }), + [queryClient, selected.month, selected.year], + ); + // Seed the editable form fields (starting amounts, income) from the loaded + // summary and reset transient edit/drag state whenever the data changes — + // this is what loadSummary used to do inline. useEffect(() => { - loadSummary(); - }, [loadSummary]); + if (!data) return; + setStartingFirst(String(data.starting_amounts?.first_amount ?? 0)); + setStartingFifteenth(String(data.starting_amounts?.fifteenth_amount ?? 0)); + setStartingOther(String(data.starting_amounts?.other_amount ?? 0)); + setEditingStarting(false); + setIncomeAmount(String(data.income?.amount ?? 0)); + setIncomeLabel(data.income?.label || 'Salary'); + setEditingIncome(false); + setDraggingId(null); + setDropTargetId(null); + setMovingBillId(null); + }, [data]); const summary = data?.summary || {}; const expenses = data?.expenses || [];