fix(client): resolve all 13 exhaustive-deps warnings (R2b)

- BankSyncSection: handleBtSave read btLateGraceDays but it was missing from the
  useCallback deps -> saving could persist a STALE late-grace value (real bug).
- ProfilePage: EditProfile sync effect now depends on [profile].
- Stabilized identity of derived arrays/objects feeding useMemo deps (they were
  recreated every render, defeating memoization): TrackerPage filters + rows,
  HealthPage bills, BankTransactionsPage transactions -> wrapped in useMemo.
- BillModal + TransactionMatchingSection: intentional id/filter-scoped effects
  documented with a targeted eslint-disable + reason.
- Removed 2 stale eslint-disable directives (confirm-dialog, useAuth).

exhaustive-deps + rules-of-hooks now both 0. Build + client tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
null 2026-07-03 19:47:14 -05:00
parent 5e267e4fa7
commit b8d394061b
9 changed files with 15 additions and 9 deletions

View File

@ -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

View File

@ -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);

View File

@ -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(() => {

View File

@ -157,7 +157,6 @@ export function useConfirm() {
onConfirm={handleConfirm}
/>
),
// eslint-disable-next-line react-hooks/exhaustive-deps
[state, handleConfirm, handleOpenChange]
);

View File

@ -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();

View File

@ -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));

View File

@ -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;

View File

@ -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);

View File

@ -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;