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:
parent
a0e4f87fe1
commit
a37697d492
|
|
@ -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'],
|
||||
|
|
|
|||
|
|
@ -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 || [];
|
||||
|
|
|
|||
Loading…
Reference in New Issue