diff --git a/client/hooks/useQueries.js b/client/hooks/useQueries.js index be00230..10e6da7 100644 --- a/client/hooks/useQueries.js +++ b/client/hooks/useQueries.js @@ -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], diff --git a/client/pages/BankTransactionsPage.jsx b/client/pages/BankTransactionsPage.jsx index 8ff6b5c..7c3b2cf 100644 --- a/client/pages/BankTransactionsPage.jsx +++ b/client/pages/BankTransactionsPage.jsx @@ -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() { Auto-categorize -