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,
|
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 { 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);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue