perf(ui): useDeferredValue on the Bills + Subscriptions search filters (Phase 2 stack leverage)
Both pages filter the whole list on every keystroke. The input now updates instantly while the (deferred) filter+render lags behind — React 19's useDeferredValue, which the app wasn't using anywhere. Typing stays snappy on large lists; zero behavior change otherwise. typecheck/lint/build clean, probe 17/17. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
57e7ede670
commit
a6bc85b18c
|
|
@ -1,4 +1,4 @@
|
||||||
import { useEffect, useState, useCallback, useMemo, useRef } from 'react';
|
import { useEffect, useState, useCallback, useMemo, useRef, useDeferredValue } from 'react';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { useBills, useCategories, useBillTemplates, useDeletedBills } from '@/hooks/useQueries';
|
import { useBills, useCategories, useBillTemplates, useDeletedBills } from '@/hooks/useQueries';
|
||||||
|
|
@ -676,6 +676,9 @@ export default function BillsPage() {
|
||||||
const [showDeleted, setShowDeleted] = useState(false);
|
const [showDeleted, setShowDeleted] = useState(false);
|
||||||
const [showInactive, setShowInactive] = useState(false);
|
const [showInactive, setShowInactive] = useState(false);
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
|
// Keep typing responsive: the input updates `search` instantly while the
|
||||||
|
// (whole-list) filter below runs against the deferred value (React 19).
|
||||||
|
const deferredSearch = useDeferredValue(search);
|
||||||
const [draggingId, setDraggingId] = useState<number | null>(null);
|
const [draggingId, setDraggingId] = useState<number | null>(null);
|
||||||
const [dropTargetId, setDropTargetId] = useState<number | null>(null);
|
const [dropTargetId, setDropTargetId] = useState<number | null>(null);
|
||||||
const [movingBillId, setMovingBillId] = useState<number | string | null>(null);
|
const [movingBillId, setMovingBillId] = useState<number | string | null>(null);
|
||||||
|
|
@ -860,7 +863,7 @@ export default function BillsPage() {
|
||||||
), [bills]);
|
), [bills]);
|
||||||
|
|
||||||
const filteredBills = useMemo(() => {
|
const filteredBills = useMemo(() => {
|
||||||
const q = search.trim().toLowerCase();
|
const q = deferredSearch.trim().toLowerCase();
|
||||||
return bills.filter(bill => {
|
return bills.filter(bill => {
|
||||||
if (filters.inactive && bill.active) return false;
|
if (filters.inactive && bill.active) return false;
|
||||||
if (filters.category !== FILTER_ALL && String(bill.category_id ?? '') !== filters.category) return false;
|
if (filters.category !== FILTER_ALL && String(bill.category_id ?? '') !== filters.category) return false;
|
||||||
|
|
@ -882,7 +885,7 @@ export default function BillsPage() {
|
||||||
].filter(Boolean).join(' ').toLowerCase();
|
].filter(Boolean).join(' ').toLowerCase();
|
||||||
return haystack.includes(q);
|
return haystack.includes(q);
|
||||||
});
|
});
|
||||||
}, [bills, filters, search]);
|
}, [bills, filters, deferredSearch]);
|
||||||
|
|
||||||
const active = filteredBills.filter(b => b.active);
|
const active = filteredBills.filter(b => b.active);
|
||||||
const inactive = filteredBills.filter(b => !b.active);
|
const inactive = filteredBills.filter(b => !b.active);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useCallback, useEffect, useMemo, useOptimistic, useRef, useState, type ComponentType, type ComponentProps, type ReactNode } from 'react';
|
import { useCallback, useDeferredValue, useEffect, useMemo, useOptimistic, useRef, useState, type ComponentType, type ComponentProps, type ReactNode } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { useSubscriptions, useSubscriptionRecommendations, useCategories, useBills } from '@/hooks/useQueries';
|
import { useSubscriptions, useSubscriptionRecommendations, useCategories, useBills } from '@/hooks/useQueries';
|
||||||
|
|
@ -1348,8 +1348,10 @@ export default function SubscriptionsPage() {
|
||||||
useCallback((state: Subscription[], { id, active }: { id: number; active: boolean }) => state.map(s => s.id === id ? { ...s, active } : s), []),
|
useCallback((state: Subscription[], { id, active }: { id: number; active: boolean }) => state.map(s => s.id === id ? { ...s, active } : s), []),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Deferred so typing in the search box stays snappy while the list re-filters.
|
||||||
|
const deferredSubSearch = useDeferredValue(subSearch);
|
||||||
const filteredSubscriptions = useMemo(() => {
|
const filteredSubscriptions = useMemo(() => {
|
||||||
const q = subSearch.trim().toLowerCase();
|
const q = deferredSubSearch.trim().toLowerCase();
|
||||||
const base = q
|
const base = q
|
||||||
? optimisticSubs.filter(item =>
|
? optimisticSubs.filter(item =>
|
||||||
String(item.name || '').toLowerCase().includes(q) ||
|
String(item.name || '').toLowerCase().includes(q) ||
|
||||||
|
|
@ -1358,7 +1360,7 @@ export default function SubscriptionsPage() {
|
||||||
)
|
)
|
||||||
: optimisticSubs;
|
: optimisticSubs;
|
||||||
return base;
|
return base;
|
||||||
}, [optimisticSubs, subSearch]);
|
}, [optimisticSubs, deferredSubSearch]);
|
||||||
const active = filteredSubscriptions.filter(item => item.active);
|
const active = filteredSubscriptions.filter(item => item.active);
|
||||||
const paused = filteredSubscriptions.filter(item => !item.active);
|
const paused = filteredSubscriptions.filter(item => !item.active);
|
||||||
const sortedActive = useMemo(
|
const sortedActive = useMemo(
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue