From bb024ce161692585527afb18994a20b367991b9a Mon Sep 17 00:00:00 2001
From: null
Date: Fri, 3 Jul 2026 20:04:14 -0500
Subject: [PATCH] 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
---
client/hooks/useQueries.js | 16 ++++++++++++++-
client/pages/AnalyticsPage.jsx | 36 ++++++++--------------------------
2 files changed, 23 insertions(+), 29 deletions(-)
diff --git a/client/hooks/useQueries.js b/client/hooks/useQueries.js
index 2a66c6e..4ab3d83 100644
--- a/client/hooks/useQueries.js
+++ b/client/hooks/useQueries.js
@@ -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,
+ });
+}
diff --git a/client/pages/AnalyticsPage.jsx b/client/pages/AnalyticsPage.jsx
index cc4348f..211dff9 100644
--- a/client/pages/AnalyticsPage.jsx
+++ b/client/pages/AnalyticsPage.jsx
@@ -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() {
-