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:
parent
9cb254ea13
commit
a0e4f87fe1
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in New Issue