refactor(bank): migrate BankTransactionsPage ledger to React Query (R5.7)
The paginated/filtered ledger (the race-prone data) moves to a useBankLedger query keyed on account/flow/page/query/sort — React Query handles caching, dedup, cancellation and out-of-order responses, replacing the manual request-id guard. Optimistic categorize routes through a setLedger setQueryData wrapper; loadLedger is the query's refetch (mutations + Refresh); the refresh button uses isFetching. Mount-once categories/bills stay local loads. This completes R5 — all 7 manual-fetch pages are on React Query. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
2e8bc07552
commit
6802a66e82
|
|
@ -110,6 +110,23 @@ export function useSummary(year, month) {
|
|||
});
|
||||
}
|
||||
|
||||
export function useBankLedger({ accountId, flow, page, query, sortBy, sortDir, pageSize }) {
|
||||
return useQuery({
|
||||
queryKey: ['bank-ledger', accountId, flow, page, query, sortBy, sortDir],
|
||||
queryFn: () => api.bankTransactionsLedger({
|
||||
limit: pageSize,
|
||||
offset: (page - 1) * pageSize,
|
||||
q: query,
|
||||
account_id: accountId === 'all' ? undefined : accountId,
|
||||
flow,
|
||||
sort_by: sortBy,
|
||||
sort_dir: sortDir,
|
||||
}),
|
||||
staleTime: 1000 * 60,
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
}
|
||||
|
||||
export function useSpendingSummary(year, month) {
|
||||
return useQuery({
|
||||
queryKey: ['spending-summary', year, month],
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Fragment, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useBankLedger } from '@/hooks/useQueries';
|
||||
import { toast } from 'sonner';
|
||||
import {
|
||||
ArrowDown,
|
||||
|
|
@ -307,9 +309,7 @@ function TransactionMobileCard({ tx, categories, onCategorize, onApplySuggestion
|
|||
}
|
||||
|
||||
export default function BankTransactionsPage() {
|
||||
const [ledger, setLedger] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
const queryClient = useQueryClient();
|
||||
const [search, setSearch] = useState('');
|
||||
const [query, setQuery] = useState('');
|
||||
const [accountId, setAccountId] = useState('all');
|
||||
|
|
@ -327,9 +327,14 @@ export default function BankTransactionsPage() {
|
|||
const [createBillSourceTx, setCreateBillSourceTx] = useState(null);
|
||||
const [selectedIds, setSelectedIds] = useState(() => new Set());
|
||||
const [bulkActing, setBulkActing] = useState(false);
|
||||
// Monotonic request id: a response only lands if it belongs to the newest
|
||||
// request, so a slow Refresh can never overwrite fresher filter results.
|
||||
const requestSeq = useRef(0);
|
||||
// React Query keys the ledger on the filters/page, so it handles caching,
|
||||
// dedup, cancellation, and out-of-order responses (no manual request id).
|
||||
const { data: ledger = null, isPending: loading, isFetching, error: ledgerErr, refetch: loadLedger } =
|
||||
useBankLedger({ accountId, flow, page, query, sortBy, sortDir, pageSize: PAGE_SIZE });
|
||||
const error = ledgerErr ? (ledgerErr.message || 'Unable to load bank transactions') : '';
|
||||
const setLedger = useCallback((u) => queryClient.setQueryData(
|
||||
['bank-ledger', accountId, flow, page, query, sortBy, sortDir],
|
||||
prev => (typeof u === 'function' ? u(prev) : u)), [queryClient, accountId, flow, page, query, sortBy, sortDir]);
|
||||
|
||||
useEffect(() => {
|
||||
const id = window.setTimeout(() => {
|
||||
|
|
@ -352,30 +357,6 @@ export default function BankTransactionsPage() {
|
|||
api.bills().then(data => setBills(data || [])).catch(() => setBills([]));
|
||||
}, []);
|
||||
|
||||
// Single fetch path — used by both the load effect and the Refresh button.
|
||||
const loadLedger = useCallback(async () => {
|
||||
const seq = ++requestSeq.current;
|
||||
setLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
const data = await api.bankTransactionsLedger({
|
||||
limit: PAGE_SIZE,
|
||||
offset: (page - 1) * PAGE_SIZE,
|
||||
q: query,
|
||||
account_id: accountId === 'all' ? undefined : accountId,
|
||||
flow,
|
||||
sort_by: sortBy,
|
||||
sort_dir: sortDir,
|
||||
});
|
||||
if (seq === requestSeq.current) setLedger(data);
|
||||
} catch (err) {
|
||||
if (seq === requestSeq.current) setError(err.message || 'Unable to load bank transactions');
|
||||
} finally {
|
||||
if (seq === requestSeq.current) setLoading(false);
|
||||
}
|
||||
}, [accountId, flow, page, query, sortBy, sortDir]);
|
||||
|
||||
useEffect(() => { loadLedger(); }, [loadLedger]);
|
||||
|
||||
const updateTransaction = useCallback((id, patch) => {
|
||||
setLedger(prev => {
|
||||
|
|
@ -385,7 +366,7 @@ export default function BankTransactionsPage() {
|
|||
transactions: prev.transactions.map(tx => (tx.id === id ? { ...tx, ...patch } : tx)),
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
}, [setLedger]);
|
||||
|
||||
const handleCategorize = useCallback(async (tx, categoryId) => {
|
||||
try {
|
||||
|
|
@ -739,7 +720,7 @@ export default function BankTransactionsPage() {
|
|||
<Sparkles className={cn('h-4 w-4', autoCategorizing && 'animate-pulse')} />
|
||||
Auto-categorize
|
||||
</Button>
|
||||
<Button type="button" variant="outline" onClick={loadLedger} disabled={loading}>
|
||||
<Button type="button" variant="outline" onClick={loadLedger} disabled={isFetching}>
|
||||
<RefreshCw className={cn('h-4 w-4', loading && 'animate-spin')} />
|
||||
Refresh
|
||||
</Button>
|
||||
|
|
|
|||
Loading…
Reference in New Issue