diff --git a/client/components/BillModal.jsx b/client/components/BillModal.jsx index 808568d..2dc8d12 100644 --- a/client/components/BillModal.jsx +++ b/client/components/BillModal.jsx @@ -176,6 +176,9 @@ export default function BillModal({ bill, initialBill, categories, onClose, onSa useEffect(() => { loadPayments(); loadLinkedTransactions(); + // Intentional: reload only when the bill identity changes. The loaders are + // recreated each render, so listing them would reload on every render. + // eslint-disable-next-line react-hooks/exhaustive-deps }, [bill?.id]); // Imported payments (via sync or a merchant-rule historical import) must diff --git a/client/components/data/BankSyncSection.jsx b/client/components/data/BankSyncSection.jsx index 181aca2..3a5b0ef 100644 --- a/client/components/data/BankSyncSection.jsx +++ b/client/components/data/BankSyncSection.jsx @@ -380,7 +380,7 @@ export default function BankSyncSection({ onConnectionChange, cardProps = {} }) } finally { setBtSaving(false); } - }, [btEnabled, btAccountId, btPendingDays]); + }, [btEnabled, btAccountId, btPendingDays, btLateGraceDays]); const handleAcmSave = useCallback(async (next) => { setAcmSaving(true); diff --git a/client/components/data/TransactionMatchingSection.jsx b/client/components/data/TransactionMatchingSection.jsx index 7fb97c4..30fb612 100644 --- a/client/components/data/TransactionMatchingSection.jsx +++ b/client/components/data/TransactionMatchingSection.jsx @@ -256,6 +256,8 @@ export default function TransactionMatchingSection({ refreshKey, simplefinConn, }; useEffect(() => { loadBills(); }, []); + // Intentional: reset + reload page 1 when the filter or refresh key changes. + // eslint-disable-next-line react-hooks/exhaustive-deps useEffect(() => { setSearch(''); setPage(1); loadTransactions(1, ''); }, [filter, refreshKey]); useEffect(() => { loadSuggestions(); }, [refreshKey]); useEffect(() => { diff --git a/client/components/ui/confirm-dialog.jsx b/client/components/ui/confirm-dialog.jsx index e22da48..cda4ec8 100644 --- a/client/components/ui/confirm-dialog.jsx +++ b/client/components/ui/confirm-dialog.jsx @@ -157,7 +157,6 @@ export function useConfirm() { onConfirm={handleConfirm} /> ), - // eslint-disable-next-line react-hooks/exhaustive-deps [state, handleConfirm, handleOpenChange] ); diff --git a/client/hooks/useAuth.jsx b/client/hooks/useAuth.jsx index d448b41..e2fd33d 100644 --- a/client/hooks/useAuth.jsx +++ b/client/hooks/useAuth.jsx @@ -20,7 +20,7 @@ export function AuthProvider({ children }) { }).catch(err => console.error('[useAuth] authMode check failed', err)); api.me().then(applyMeResponse).catch(() => setUser(null)); - }, []); // eslint-disable-line + }, []); const logout = async () => { await api.logout(); diff --git a/client/pages/BankTransactionsPage.jsx b/client/pages/BankTransactionsPage.jsx index 627d640..8ff6b5c 100644 --- a/client/pages/BankTransactionsPage.jsx +++ b/client/pages/BankTransactionsPage.jsx @@ -545,7 +545,7 @@ export default function BankTransactionsPage() { }, [updateTransaction, loadLedger]); const accounts = ledger?.accounts || []; - const transactions = ledger?.transactions || []; + const transactions = useMemo(() => ledger?.transactions || [], [ledger]); const summary = ledger?.summary || {}; const total = Number(ledger?.total || 0); const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE)); diff --git a/client/pages/HealthPage.jsx b/client/pages/HealthPage.jsx index c10d78b..1bb09f9 100644 --- a/client/pages/HealthPage.jsx +++ b/client/pages/HealthPage.jsx @@ -161,7 +161,7 @@ export default function HealthPage() { }, [load]); const summary = data?.summary || {}; - const bills = data?.bills || []; + const bills = useMemo(() => data?.bills || [], [data]); const sortedBills = useMemo(() => [...bills].sort((a, b) => { const aErrors = a.issues.filter(issue => issue.severity === 'error').length; const bErrors = b.issues.filter(issue => issue.severity === 'error').length; diff --git a/client/pages/ProfilePage.jsx b/client/pages/ProfilePage.jsx index 5919012..a72d9b7 100644 --- a/client/pages/ProfilePage.jsx +++ b/client/pages/ProfilePage.jsx @@ -333,7 +333,7 @@ function EditProfile({ profile, onSaved }) { useEffect(() => { setDisplayName(displayNameOf(profile)); - }, [profile.display_name, profile.displayName, profile.name]); + }, [profile]); const save = async () => { setSaving(true); diff --git a/client/pages/TrackerPage.jsx b/client/pages/TrackerPage.jsx index 119ccae..06207bb 100644 --- a/client/pages/TrackerPage.jsx +++ b/client/pages/TrackerPage.jsx @@ -205,7 +205,9 @@ export default function TrackerPage() { const year = Number(searchParams.get('year')) || now.getFullYear(); const month = Number(searchParams.get('month')) || (now.getMonth() + 1); const search = searchParams.get('q') || ''; - const filters = { + // Stable identity so the memo that filters rows on `filters` doesn't recompute + // every render (a new object literal each render would defeat it). + const filters = useMemo(() => ({ category: searchParams.get('fc') || FILTER_ALL, cycle: searchParams.get('cy') || FILTER_ALL, autopay: searchParams.get('ap') === '1', @@ -214,7 +216,7 @@ export default function TrackerPage() { unpaid: searchParams.get('un') === '1', overdue: searchParams.get('ov') === '1', debt: searchParams.get('de') === '1', - }; + }), [searchParams]); const sortKey = normalizeTrackerSortKey(searchParams.get('sort') || TRACKER_SORT_DEFAULT); const hasSort = sortKey !== TRACKER_SORT_DEFAULT; const sortDir = hasSort @@ -381,7 +383,7 @@ export default function TrackerPage() { updateParams({ year: n.getFullYear(), month: n.getMonth() + 1 }); } - const rows = orderedRows || data?.rows || []; + const rows = useMemo(() => orderedRows || data?.rows || [], [orderedRows, data]); const summary = data?.summary || {}; const bankTracking = data?.bank_tracking; const cashflow = data?.cashflow;