refactor(subscriptions): migrate SubscriptionsPage to React Query (R5.3)

useSubscriptions + useSubscriptionRecommendations (+ shared useBills/
useCategories). Optimistic updates (toggle, reorder, dismiss recommendation)
and their await-load() reconciliation preserved by routing setData/setBills/
setRecommendations through queryClient.setQueryData and load()/loadRecommendations
through invalidateQueries. The redundant mount-load effect was removed (hooks
fetch on mount). useOptimistic layer unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
null 2026-07-03 20:12:02 -05:00
parent 9cb254ea13
commit a0e4f87fe1
2 changed files with 41 additions and 44 deletions

View File

@ -97,3 +97,19 @@ export function useDeletedBills() {
staleTime: 1000 * 60 * 5,
});
}
export function useSubscriptions() {
return useQuery({
queryKey: ['subscriptions'],
queryFn: () => api.subscriptions(),
staleTime: 1000 * 60 * 5,
});
}
export function useSubscriptionRecommendations() {
return useQuery({
queryKey: ['subscription-recommendations'],
queryFn: () => api.subscriptionRecommendations().then(r => r.recommendations || []),
staleTime: 1000 * 60 * 5,
});
}

View File

@ -1,5 +1,7 @@
import { useCallback, useEffect, useMemo, useOptimistic, useRef, useState } from 'react';
import { Link } from 'react-router-dom';
import { useQueryClient } from '@tanstack/react-query';
import { useSubscriptions, useSubscriptionRecommendations, useCategories, useBills } from '@/hooks/useQueries';
import { toast } from 'sonner';
import {
Bell,
@ -993,12 +995,20 @@ function RecommendationDetailsDialog({ open, recommendation, categoryId, onClose
}
export default function SubscriptionsPage() {
const [data, setData] = useState({ summary: {}, subscriptions: [] });
const [recommendations, setRecommendations] = useState([]);
const [categories, setCategories] = useState([]);
const [bills, setBills] = useState([]);
const [loading, setLoading] = useState(true);
const [recommendationsLoading, setRecommendationsLoading] = useState(true);
const queryClient = useQueryClient();
const { data: subData, isPending: loading } = useSubscriptions();
const data = subData || { summary: {}, subscriptions: [] };
const { data: recommendations = [], isPending: recommendationsLoading } = useSubscriptionRecommendations();
const { data: categories = [] } = useCategories();
const { data: bills = [] } = useBills();
// Optimistic updates write through the query cache so every existing call site
// (setData(prev => ), setBills(), setRecommendations()) works unchanged.
const setData = useCallback((u) => queryClient.setQueryData(['subscriptions'],
prev => (typeof u === 'function' ? u(prev || { summary: {}, subscriptions: [] }) : u)), [queryClient]);
const setBills = useCallback((u) => queryClient.setQueryData(['bills'],
prev => (typeof u === 'function' ? u(prev || []) : u)), [queryClient]);
const setRecommendations = useCallback((u) => queryClient.setQueryData(['subscription-recommendations'],
prev => (typeof u === 'function' ? u(prev || []) : u)), [queryClient]);
const [busyId, setBusyId] = useState(null);
const [modal, setModal] = useState(null);
const [matchTarget, setMatchTarget] = useState(null);
@ -1027,44 +1037,15 @@ export default function SubscriptionsPage() {
return match?.id || null;
}, [categories]);
const load = useCallback(async () => {
setLoading(true);
try {
const [subscriptionData, categoryData] = await Promise.all([
api.subscriptions(),
api.categories(),
]);
setData(subscriptionData);
setCategories(categoryData || []);
} catch (err) {
toast.error(err.message || 'Failed to load subscriptions.');
} finally {
setLoading(false);
}
}, []);
const loadRecommendations = useCallback(async () => {
setRecommendationsLoading(true);
try {
const result = await api.subscriptionRecommendations();
setRecommendations(result.recommendations || []);
} catch (err) {
toast.error(err.message || 'Failed to scan subscription recommendations.');
} finally {
setRecommendationsLoading(false);
}
}, []);
useEffect(() => {
load();
loadRecommendations();
api.allBills()
.then(b => setBills(Array.isArray(b) ? b : []))
.catch(err => {
console.error('[SubscriptionsPage] failed to load bills', err);
toast.error(err.message || 'Bills could not be loaded for subscription linking.');
});
}, [load, loadRecommendations]);
const load = useCallback(() => Promise.all([
queryClient.invalidateQueries({ queryKey: ['subscriptions'] }),
queryClient.invalidateQueries({ queryKey: ['categories'] }),
queryClient.invalidateQueries({ queryKey: ['bills'] }),
]), [queryClient]);
const loadRecommendations = useCallback(
() => queryClient.invalidateQueries({ queryKey: ['subscription-recommendations'] }),
[queryClient],
);
useEffect(() => {
localStorage.setItem(SUBSCRIPTION_SORT_KEY, subscriptionSort);