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, 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 { useCallback, useEffect, useMemo, useOptimistic, useRef, useState } from 'react';
import { Link } from 'react-router-dom'; 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 { toast } from 'sonner';
import { import {
Bell, Bell,
@ -993,12 +995,20 @@ function RecommendationDetailsDialog({ open, recommendation, categoryId, onClose
} }
export default function SubscriptionsPage() { export default function SubscriptionsPage() {
const [data, setData] = useState({ summary: {}, subscriptions: [] }); const queryClient = useQueryClient();
const [recommendations, setRecommendations] = useState([]); const { data: subData, isPending: loading } = useSubscriptions();
const [categories, setCategories] = useState([]); const data = subData || { summary: {}, subscriptions: [] };
const [bills, setBills] = useState([]); const { data: recommendations = [], isPending: recommendationsLoading } = useSubscriptionRecommendations();
const [loading, setLoading] = useState(true); const { data: categories = [] } = useCategories();
const [recommendationsLoading, setRecommendationsLoading] = useState(true); 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 [busyId, setBusyId] = useState(null);
const [modal, setModal] = useState(null); const [modal, setModal] = useState(null);
const [matchTarget, setMatchTarget] = useState(null); const [matchTarget, setMatchTarget] = useState(null);
@ -1027,44 +1037,15 @@ export default function SubscriptionsPage() {
return match?.id || null; return match?.id || null;
}, [categories]); }, [categories]);
const load = useCallback(async () => { const load = useCallback(() => Promise.all([
setLoading(true); queryClient.invalidateQueries({ queryKey: ['subscriptions'] }),
try { queryClient.invalidateQueries({ queryKey: ['categories'] }),
const [subscriptionData, categoryData] = await Promise.all([ queryClient.invalidateQueries({ queryKey: ['bills'] }),
api.subscriptions(), ]), [queryClient]);
api.categories(), const loadRecommendations = useCallback(
]); () => queryClient.invalidateQueries({ queryKey: ['subscription-recommendations'] }),
setData(subscriptionData); [queryClient],
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]);
useEffect(() => { useEffect(() => {
localStorage.setItem(SUBSCRIPTION_SORT_KEY, subscriptionSort); localStorage.setItem(SUBSCRIPTION_SORT_KEY, subscriptionSort);