diff --git a/client/App.jsx b/client/App.jsx
index 06ecfff..89e25d1 100644
--- a/client/App.jsx
+++ b/client/App.jsx
@@ -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 (
- <>
+
{/* Release notes (only for user role) */}
{user?.role === 'user' && }
@@ -176,6 +190,7 @@ export default function App() {
- >
+
+
);
}
diff --git a/client/hooks/useQueries.js b/client/hooks/useQueries.js
new file mode 100644
index 0000000..92a254f
--- /dev/null
+++ b/client/hooks/useQueries.js
@@ -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
+ });
+}
\ No newline at end of file
diff --git a/client/lib/version.js b/client/lib/version.js
index 7ede401..fcfc02f 100644
--- a/client/lib/version.js
+++ b/client/lib/version.js
@@ -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.' },
],
};
\ No newline at end of file
diff --git a/client/pages/TrackerPage.jsx b/client/pages/TrackerPage.jsx
index 7e340d2..1338330 100644
--- a/client/pages/TrackerPage.jsx
+++ b/client/pages/TrackerPage.jsx
@@ -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() {
)}
- {first.length > 0 && }
- {second.length > 0 && }
+ {first.length > 0 && }
+ {second.length > 0 && }
{/* 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(); }}
/>
diff --git a/package-lock.json b/package-lock.json
index de81d3a..7d8ad07 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/package.json b/package.json
index d234dbb..ace09bf 100644
--- a/package.json
+++ b/package.json
@@ -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",