refactor(analytics): migrate AnalyticsPage to React Query (R5.1)
Replaced the manual useState(data/loading/error) + load useCallback + useEffect (and the R3 request-seq guard) with a useAnalyticsSummary(params) query hook. React Query now handles caching, dedup, cancellation, and out-of-order responses via the params-encoded key; keepPreviousData keeps the last result visible while a new month/filter loads. Refresh -> refetch; the redundant page-load error toast is dropped in favor of the existing inline error state. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
65c61d71fa
commit
bb024ce161
|
|
@ -1,4 +1,4 @@
|
|||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useQuery, useQueryClient, keepPreviousData } from '@tanstack/react-query';
|
||||
import { useCallback } from 'react';
|
||||
import { api } from '@/api';
|
||||
|
||||
|
|
@ -67,3 +67,17 @@ export function useInvalidateTrackerData() {
|
|||
}
|
||||
}, [queryClient]);
|
||||
}
|
||||
|
||||
// ── Page data (migrated off manual useEffect + load()) ───────────────────────
|
||||
// The queryKey encodes the params, so React Query handles caching, request
|
||||
// dedup, cancellation, and out-of-order responses — no manual sequence guards.
|
||||
// keepPreviousData keeps the last result visible while a new month/filter loads.
|
||||
|
||||
export function useAnalyticsSummary(params) {
|
||||
return useQuery({
|
||||
queryKey: ['analytics-summary', params],
|
||||
queryFn: () => api.analyticsSummary(params),
|
||||
staleTime: 1000 * 60 * 2,
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { formatUSD, formatUSDWhole } from '@/lib/money';
|
||||
import { Printer, RefreshCw, RotateCcw } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { api } from '@/api';
|
||||
import { useAnalyticsSummary } from '@/hooks/useQueries';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Skeleton } from '@/components/ui/Skeleton';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
|
@ -490,9 +489,6 @@ export default function AnalyticsPage() {
|
|||
const [includeInactive, setIncludeInactive] = useState(false);
|
||||
const [includeSkipped, setIncludeSkipped] = useState(true);
|
||||
const [trendMode, setTrendMode] = useState('line');
|
||||
const [data, setData] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
const [visible, setVisible] = useState({
|
||||
monthlyTrend: true,
|
||||
expectedActual: true,
|
||||
|
|
@ -512,26 +508,10 @@ export default function AnalyticsPage() {
|
|||
include_skipped: includeSkipped,
|
||||
}), [billId, categoryId, includeInactive, includeSkipped, month, months, year]);
|
||||
|
||||
const requestSeq = useRef(0);
|
||||
const load = useCallback(async () => {
|
||||
// Ignore out-of-order responses: only the newest request updates state, so a
|
||||
// slow response for old params can't overwrite fresh data (or setState after
|
||||
// unmount). Same guard the Bank ledger uses.
|
||||
const seq = ++requestSeq.current;
|
||||
setLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
const result = await api.analyticsSummary(params);
|
||||
if (seq === requestSeq.current) setData(result);
|
||||
} catch (err) {
|
||||
if (seq === requestSeq.current) setError(err.message || 'Failed to load analytics.');
|
||||
toast.error(err.message || 'Failed to load analytics.');
|
||||
} finally {
|
||||
if (seq === requestSeq.current) setLoading(false);
|
||||
}
|
||||
}, [params]);
|
||||
|
||||
useEffect(() => { load(); }, [load]);
|
||||
// React Query handles caching, dedup, cancellation, and out-of-order responses.
|
||||
const { data, isPending, isFetching, error: queryError, refetch } = useAnalyticsSummary(params);
|
||||
const loading = isPending;
|
||||
const error = queryError ? (queryError.message || 'Failed to load analytics.') : '';
|
||||
|
||||
const forecastRows = useMemo(
|
||||
() => linearForecast(data?.monthly_spending || [], forecastHorizon),
|
||||
|
|
@ -579,8 +559,8 @@ export default function AnalyticsPage() {
|
|||
</p>
|
||||
</div>
|
||||
<div className="analytics-actions flex flex-wrap gap-2">
|
||||
<Button variant="outline" size="sm" onClick={load} disabled={loading} className="flex-1 sm:flex-none">
|
||||
<RefreshCw className={cn('h-3.5 w-3.5', loading && 'animate-spin')} />
|
||||
<Button variant="outline" size="sm" onClick={() => refetch()} disabled={isFetching} className="flex-1 sm:flex-none">
|
||||
<RefreshCw className={cn('h-3.5 w-3.5', isFetching && 'animate-spin')} />
|
||||
Refresh
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={() => window.print()} className="flex-1 sm:flex-none">
|
||||
|
|
|
|||
Loading…
Reference in New Issue