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
-