feat(tracker): prefetch adjacent month on nav hover for instant switching (P2)

usePrefetchTracker() warms the ['tracker', y, m] cache when the user hovers/
focuses the prev/next month buttons, so clicking is instant (no round-trip).
No-op if already cached and fresh.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
null 2026-07-03 20:58:23 -05:00
parent 2793927a5c
commit e941f05cd6
2 changed files with 31 additions and 3 deletions

View File

@ -68,6 +68,19 @@ export function useInvalidateTrackerData() {
}, [queryClient]); }, [queryClient]);
} }
// Prefetch a tracker month into the cache (e.g. on month-nav hover) so switching
// to it is instant. No-op if it's already cached and fresh.
export function usePrefetchTracker() {
const queryClient = useQueryClient();
return useCallback((year, month) => {
queryClient.prefetchQuery({
queryKey: ['tracker', year, month],
queryFn: () => api.tracker(year, month),
staleTime: 1000 * 60 * 5,
});
}, [queryClient]);
}
// ── Page data (migrated off manual useEffect + load()) ─────────────────────── // ── Page data (migrated off manual useEffect + load()) ───────────────────────
// The queryKey encodes the params, so React Query handles caching, request // The queryKey encodes the params, so React Query handles caching, request
// dedup, cancellation, and out-of-order responses — no manual sequence guards. // dedup, cancellation, and out-of-order responses — no manual sequence guards.

View File

@ -3,7 +3,7 @@ import { useSearchParams } from 'react-router-dom';
import { ChevronLeft, ChevronRight, AlertCircle, CheckCircle2, Plus, Search, RefreshCw, Landmark, ArrowUpToLine, ArrowUp, ArrowDown, BellOff, EyeOff, Settings2 } from 'lucide-react'; import { ChevronLeft, ChevronRight, AlertCircle, CheckCircle2, Plus, Search, RefreshCw, Landmark, ArrowUpToLine, ArrowUp, ArrowDown, BellOff, EyeOff, Settings2 } from 'lucide-react';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { api } from '@/api.js'; import { api } from '@/api.js';
import { useTracker, useDriftReport, useInvalidateTrackerData } from '@/hooks/useQueries'; import { useTracker, useDriftReport, useInvalidateTrackerData, usePrefetchTracker } from '@/hooks/useQueries';
import { useSearchPanelPreference } from '@/hooks/useSearchPanelPreference'; import { useSearchPanelPreference } from '@/hooks/useSearchPanelPreference';
import BillModal from '@/components/BillModal'; import BillModal from '@/components/BillModal';
import { makeBillDraft } from '@/lib/billDrafts'; import { makeBillDraft } from '@/lib/billDrafts';
@ -301,12 +301,23 @@ export default function TrackerPage() {
return () => window.removeEventListener('tracker:late-attributions', handler); return () => window.removeEventListener('tracker:late-attributions', handler);
}, []); }, []);
function navigate(delta) { function adjacentMonth(delta) {
let nm = month + delta; let nm = month + delta;
let ny = year; let ny = year;
if (nm > 12) { ny += 1; nm = 1; } if (nm > 12) { ny += 1; nm = 1; }
if (nm < 1) { ny -= 1; nm = 12; } if (nm < 1) { ny -= 1; nm = 12; }
updateParams({ year: ny, month: nm }); return { year: ny, month: nm };
}
function navigate(delta) {
updateParams(adjacentMonth(delta));
}
// Prefetch the adjacent month on hover/focus so clicking prev/next is instant.
const prefetchTracker = usePrefetchTracker();
function prefetchAdjacent(delta) {
const { year: ny, month: nm } = adjacentMonth(delta);
prefetchTracker(ny, nm);
} }
function bankSyncMessage(result) { function bankSyncMessage(result) {
@ -651,6 +662,8 @@ export default function TrackerPage() {
<Button <Button
size="icon" variant="ghost" size="icon" variant="ghost"
onClick={() => navigate(-1)} onClick={() => navigate(-1)}
onMouseEnter={() => prefetchAdjacent(-1)}
onFocus={() => prefetchAdjacent(-1)}
className="h-7 w-7 hover:bg-white/5" className="h-7 w-7 hover:bg-white/5"
aria-label="Previous month" aria-label="Previous month"
> >
@ -662,6 +675,8 @@ export default function TrackerPage() {
<Button <Button
size="icon" variant="ghost" size="icon" variant="ghost"
onClick={() => navigate(1)} onClick={() => navigate(1)}
onMouseEnter={() => prefetchAdjacent(1)}
onFocus={() => prefetchAdjacent(1)}
className="h-7 w-7 hover:bg-white/5" className="h-7 w-7 hover:bg-white/5"
aria-label="Next month" aria-label="Next month"
> >