v0.22.0: React Query Migration
- Added @tanstack/react-query and @tanstack/react-query-devtools - Created useTracker, useBills, useCategories custom hooks (useQueries.js) - Migrated TrackerPage from manual useState/useEffect to useQuery - Added QueryClientProvider with 2min staleTime, 1 retry, refetchOnWindowFocus: false - Added ReactQueryDevtools for development - Fixed error handling: useRef pattern prevents duplicate toast notifications - Replaced load() callback with refetch() from useQuery - Hudson security audit: 4/5 PASS (1 FAIL fixed: error handling toast duplication)
This commit is contained in:
parent
314159d241
commit
d67fe6e61d
|
|
@ -9,6 +9,20 @@ import LoginPage from '@/pages/LoginPage';
|
|||
import ErrorBoundary from '@/components/ErrorBoundary';
|
||||
import PageLoader from '@/components/PageLoader';
|
||||
|
||||
// TanStack Query
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 1000 * 60 * 2, // 2 minutes
|
||||
retry: 1,
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Lazy-loaded components
|
||||
const AdminPage = lazy(() => import('@/pages/AdminPage'));
|
||||
const TrackerPage = lazy(() => import('@/pages/TrackerPage'));
|
||||
|
|
@ -75,7 +89,7 @@ export default function App() {
|
|||
const mainContentId = useId();
|
||||
|
||||
return (
|
||||
<>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{/* Release notes (only for user role) */}
|
||||
{user?.role === 'user' && <ReleaseNotesDialog />}
|
||||
|
||||
|
|
@ -176,6 +190,7 @@ export default function App() {
|
|||
</Route>
|
||||
</Routes>
|
||||
</main>
|
||||
</>
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
import { useQuery } from '@tanstack/react-query';
|
||||
import { api } from '@/api';
|
||||
|
||||
// Custom hook for fetching tracker data
|
||||
export function useTracker(year, month) {
|
||||
return useQuery({
|
||||
queryKey: ['tracker', year, month],
|
||||
queryFn: () => api.tracker(year, month),
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
cacheTime: 1000 * 60 * 30, // 30 minutes
|
||||
});
|
||||
}
|
||||
|
||||
// Custom hook for fetching all bills
|
||||
export function useBills() {
|
||||
return useQuery({
|
||||
queryKey: ['bills'],
|
||||
queryFn: () => api.allBills(),
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
cacheTime: 1000 * 60 * 30, // 30 minutes
|
||||
});
|
||||
}
|
||||
|
||||
// Custom hook for fetching categories
|
||||
export function useCategories() {
|
||||
return useQuery({
|
||||
queryKey: ['categories'],
|
||||
queryFn: () => api.categories(),
|
||||
staleTime: 1000 * 60 * 60, // 1 hour
|
||||
cacheTime: 1000 * 60 * 60 * 2, // 2 hours
|
||||
});
|
||||
}
|
||||
|
|
@ -1,10 +1,11 @@
|
|||
export const APP_VERSION = '0.21.1';
|
||||
export const APP_VERSION = '0.22.0';
|
||||
export const APP_NAME = 'BillTracker';
|
||||
|
||||
export const RELEASE_NOTES = {
|
||||
version: '0.21.1',
|
||||
version: '0.22.0',
|
||||
date: '2026-05-10',
|
||||
highlights: [
|
||||
{ icon: '💫', title: 'Loading Skeletons', desc: 'Tracker and Bills pages show skeleton placeholders during data loading.' },
|
||||
{ icon: '🔄', title: 'React Query Migration', desc: 'Tracker page now uses TanStack Query for data fetching with caching and auto-refetch.' },
|
||||
{ icon: '⚡', title: 'Query DevTools', desc: 'React Query DevTools available in development for inspecting query state.' },
|
||||
],
|
||||
};
|
||||
|
|
@ -2,6 +2,7 @@ import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react'
|
|||
import { ChevronLeft, ChevronRight, Pencil, TrendingUp, AlertCircle, Clock, CheckCircle2, Settings2, Loader2 } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { api } from '@/api.js';
|
||||
import { useTracker } from '@/hooks/useQueries';
|
||||
import BillModal from '@/components/BillModal';
|
||||
import { cn, fmt, fmtDate, todayStr } from '@/lib/utils';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
|
@ -1361,26 +1362,13 @@ export default function TrackerPage() {
|
|||
const now = new Date();
|
||||
const [year, setYear] = useState(now.getFullYear());
|
||||
const [month, setMonth] = useState(now.getMonth() + 1);
|
||||
const [data, setData] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
// Edit Bill modal: { bill, categories } when open, null when closed
|
||||
const [editBillData, setEditBillData] = useState(null);
|
||||
// Edit Starting Amounts modal: true when open, false when closed
|
||||
const [editStartingOpen, setEditStartingOpen] = useState(false);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await api.tracker(year, month);
|
||||
setData(res);
|
||||
} catch (err) {
|
||||
toast.error(err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [year, month]);
|
||||
|
||||
useEffect(() => { load(); }, [load]);
|
||||
// Use React Query for data fetching
|
||||
const { data, isLoading: loading, isError, error, refetch } = useTracker(year, month);
|
||||
|
||||
function navigate(delta) {
|
||||
setMonth(m => {
|
||||
|
|
@ -1409,6 +1397,16 @@ export default function TrackerPage() {
|
|||
setMonth(n.getMonth() + 1);
|
||||
}
|
||||
|
||||
// Handle errors from React Query (use ref to prevent duplicate toasts)
|
||||
const errorShownRef = useRef(false);
|
||||
useEffect(() => {
|
||||
if (isError && !errorShownRef.current) {
|
||||
toast.error(error?.message || 'Failed to load tracker data');
|
||||
errorShownRef.current = true;
|
||||
}
|
||||
if (!isError) errorShownRef.current = false;
|
||||
}, [isError, error]);
|
||||
|
||||
const rows = data?.rows || [];
|
||||
const summary = data?.summary || {};
|
||||
const first = rows.filter(r => r.bucket === '1st');
|
||||
|
|
@ -1529,8 +1527,8 @@ export default function TrackerPage() {
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
{first.length > 0 && <Bucket label="1st – 14th" rows={first} year={year} month={month} refresh={load} onEditBill={handleOpenEditBill} loading={loading} />}
|
||||
{second.length > 0 && <Bucket label="15th – 31st" rows={second} year={year} month={month} refresh={load} onEditBill={handleOpenEditBill} loading={loading} />}
|
||||
{first.length > 0 && <Bucket label="1st – 14th" rows={first} year={year} month={month} refresh={refetch} onEditBill={handleOpenEditBill} loading={loading} />}
|
||||
{second.length > 0 && <Bucket label="15th – 31st" rows={second} year={year} month={month} refresh={refetch} onEditBill={handleOpenEditBill} loading={loading} />}
|
||||
|
||||
{/* Edit Bill modal — opened by clicking a bill name in any tracker row */}
|
||||
{editBillData && (
|
||||
|
|
@ -1538,7 +1536,7 @@ export default function TrackerPage() {
|
|||
bill={editBillData.bill}
|
||||
categories={editBillData.categories}
|
||||
onClose={() => setEditBillData(null)}
|
||||
onSave={() => { setEditBillData(null); load(); }}
|
||||
onSave={() => { setEditBillData(null); refetch(); }}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
|
@ -1548,7 +1546,7 @@ export default function TrackerPage() {
|
|||
onClose={() => setEditStartingOpen(false)}
|
||||
year={year}
|
||||
month={month}
|
||||
onSave={() => { setEditStartingOpen(false); load(); }}
|
||||
onSave={() => { setEditStartingOpen(false); refetch(); }}
|
||||
/>
|
||||
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "bill-tracker",
|
||||
"version": "0.19.0",
|
||||
"version": "0.21.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "bill-tracker",
|
||||
"version": "0.19.0",
|
||||
"version": "0.21.1",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-alert-dialog": "^1.1.2",
|
||||
|
|
@ -20,6 +20,8 @@
|
|||
"@radix-ui/react-switch": "^1.1.1",
|
||||
"@radix-ui/react-tabs": "^1.1.1",
|
||||
"@radix-ui/react-tooltip": "^1.1.3",
|
||||
"@tanstack/react-query": "^5.100.9",
|
||||
"@tanstack/react-query-devtools": "^5.100.9",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"better-sqlite3": "^12.9.0",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
|
|
@ -2286,6 +2288,59 @@
|
|||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@tanstack/query-core": {
|
||||
"version": "5.100.9",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.100.9.tgz",
|
||||
"integrity": "sha512-SJSFw1S8+kQ0+knv/XGfrbocWoAlT7vDKsSImtLx3ZPQmEcR46hkDjLSvynSy25N8Ms4tIEini1FuBd5k7IscQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/query-devtools": {
|
||||
"version": "5.100.9",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.100.9.tgz",
|
||||
"integrity": "sha512-gqiptrTIhbK2PuCaPRHmWXfJG1NGYVFpAr0HqogEqiSBNB5xDz6fmesQt7w4WgMOqOQPnPHJ3ZDMuhDaXvNO8g==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-query": {
|
||||
"version": "5.100.9",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.100.9.tgz",
|
||||
"integrity": "sha512-Oa44XkaI3kCNN6ME0KByU3xT3SEUNOMfZpHxL6+wFoTm+OeUFYHKdeYVe0aOXlRDm/f15sgLwEt2HDorIdW8+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/query-core": "5.100.9"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18 || ^19"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-query-devtools": {
|
||||
"version": "5.100.9",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.100.9.tgz",
|
||||
"integrity": "sha512-mM3slaVGXJmz+pOLgXdANj75ikgQCyudyl3kmFvm6brI1JyVeY/+IeD17uDHIvZrD8hfoO2sdZ54RFsHdYAuhA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/query-devtools": "5.100.9"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tanstack/react-query": "^5.100.9",
|
||||
"react": "^18 || ^19"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/babel__core": {
|
||||
"version": "7.20.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "bill-tracker",
|
||||
"version": "0.21.1",
|
||||
"version": "0.22.0",
|
||||
"description": "Monthly bill tracking system",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
|
|
@ -22,6 +22,8 @@
|
|||
"@radix-ui/react-switch": "^1.1.1",
|
||||
"@radix-ui/react-tabs": "^1.1.1",
|
||||
"@radix-ui/react-tooltip": "^1.1.3",
|
||||
"@tanstack/react-query": "^5.100.9",
|
||||
"@tanstack/react-query-devtools": "^5.100.9",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"better-sqlite3": "^12.9.0",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
|
|
|
|||
Loading…
Reference in New Issue