From 379cc7438731ad5572efbb346d505ac00193148a Mon Sep 17 00:00:00 2001 From: Kyle Pope Date: Thu, 12 Mar 2026 22:19:08 +0800 Subject: [PATCH] Add data prefetching to eliminate skeleton flash on tab switch Prefetches all main page queries (dashboard, upcoming, todos, reminders, projects, people, locations) in parallel when the app unlocks, so the TanStack Query cache is warm before the user navigates to each tab. Co-Authored-By: Claude Opus 4.6 --- frontend/src/components/layout/AppLayout.tsx | 2 + frontend/src/hooks/usePrefetch.ts | 62 ++++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 frontend/src/hooks/usePrefetch.ts diff --git a/frontend/src/components/layout/AppLayout.tsx b/frontend/src/components/layout/AppLayout.tsx index 01870ac..aae73f7 100644 --- a/frontend/src/components/layout/AppLayout.tsx +++ b/frontend/src/components/layout/AppLayout.tsx @@ -2,6 +2,7 @@ import { useState } from 'react'; import { Outlet } from 'react-router-dom'; import { Menu } from 'lucide-react'; import { useTheme } from '@/hooks/useTheme'; +import { usePrefetch } from '@/hooks/usePrefetch'; import { AlertsProvider } from '@/hooks/useAlerts'; import { LockProvider, useLock } from '@/hooks/useLock'; import { NotificationProvider } from '@/hooks/useNotifications'; @@ -16,6 +17,7 @@ function AppContent({ mobileOpen, setMobileOpen }: { setMobileOpen: (v: boolean) => void; }) { const { isLocked, isLockResolved } = useLock(); + usePrefetch(isLockResolved && !isLocked); const [collapsed, setCollapsed] = useState(() => { try { return JSON.parse(localStorage.getItem('umbra-sidebar-collapsed') || 'false'); } catch { return false; } diff --git a/frontend/src/hooks/usePrefetch.ts b/frontend/src/hooks/usePrefetch.ts new file mode 100644 index 0000000..96dbf60 --- /dev/null +++ b/frontend/src/hooks/usePrefetch.ts @@ -0,0 +1,62 @@ +import { useEffect, useRef } from 'react'; +import { useQueryClient } from '@tanstack/react-query'; +import api from '@/lib/api'; +import { useSettings } from './useSettings'; + +/** + * Prefetches main page data in the background after the app unlocks. + * Ensures cache is warm before the user navigates to each tab, + * eliminating the skeleton flash on first visit. + */ +export function usePrefetch(enabled: boolean) { + const queryClient = useQueryClient(); + const { settings } = useSettings(); + const hasPrefetched = useRef(false); + + useEffect(() => { + if (!enabled || hasPrefetched.current) return; + hasPrefetched.current = true; + + const now = new Date(); + const clientDate = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`; + const days = settings?.upcoming_days || 7; + + // Prefetch all main page queries (no-op if already cached and fresh) + queryClient.prefetchQuery({ + queryKey: ['dashboard'], + queryFn: () => api.get(`/dashboard?client_date=${clientDate}`).then(r => r.data), + staleTime: 60_000, + }); + + queryClient.prefetchQuery({ + queryKey: ['upcoming', days], + queryFn: () => api.get(`/upcoming?days=${days}&client_date=${clientDate}`).then(r => r.data), + staleTime: 60_000, + }); + + queryClient.prefetchQuery({ + queryKey: ['todos'], + queryFn: () => api.get('/todos').then(r => r.data), + }); + + queryClient.prefetchQuery({ + queryKey: ['reminders'], + queryFn: () => api.get('/reminders').then(r => r.data), + }); + + queryClient.prefetchQuery({ + queryKey: ['projects'], + queryFn: () => api.get('/projects').then(r => r.data), + }); + + queryClient.prefetchQuery({ + queryKey: ['people'], + queryFn: () => api.get('/people').then(r => r.data), + }); + + queryClient.prefetchQuery({ + queryKey: ['locations'], + queryFn: () => api.get('/locations').then(r => r.data), + }); + }, [enabled, queryClient, settings?.upcoming_days]); +}