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 <noreply@anthropic.com>
This commit is contained in:
null 2026-07-03 20:14:31 -05:00
parent a0e4f87fe1
commit a37697d492
2 changed files with 39 additions and 33 deletions

View File

@ -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'],

View File

@ -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 || [];