A demonstration of React Query's persistent cache feature using IndexedDB. Experience instant data loading on return visits by prewarming the cache-data displays immediately while revalidating in the background.
https://www.johnnyle.io/read/never-load
Traditional web apps fetch data on every visit, showing loading states each time. With a prewarmed cache:
- First visit: Data loads normally with a 2-second simulated delay
- React Query persists the entire cache to IndexedDB
- Return visits: Cached data displays instantly (0ms) while revalidating in background
- Cache persists for 24 hours across browser sessions
This demo visualizes NYC power grid data (JulyβSeptember 2024) with charts and paginated tables to showcase the performance difference.
- π΄ Loading - First visit, fetching from server (2s delay)
- π Updating - Showing cached data while revalidating
- π’ Fresh - Cache ready and up-to-date
- β« Error - Failed to load
- Refresh - Revalidate all queries
- Clear Cache - Wipe IndexedDB and start fresh
- Pagination - Navigate pages (each page cached separately)
- Table maintains consistent height across all pages
- Pagination controls remain visible during loading
- Metadata endpoint provides page count before data loads
- Next.js 15 with App Router & Turbopack
- React 19 with TypeScript
- TanStack Query v5 (React Query) with IndexedDB persistence
- shadcn/ui (new-york style, neutral theme)
- Recharts for data visualization
- Tailwind CSS v4
- nuqs for URL state management
- Node.js 18+
- pnpm (recommended)
# Install dependencies
pnpm install
# Run development server
pnpm devOpen http://localhost:3000 to see the demo.
- First visit: Wait for data to load (~2 seconds)
- Refresh the page: Data appears instantly from cache
- Navigate pages: Each page is cached separately
- Close browser: Cache persists across sessions (24 hours)
- Click "Clear Cache": Reset to first-time experience
pnpm dev # Start dev server with Turbopack
pnpm build # Build for production
pnpm start # Start production server
pnpm lint # Run ESLintsrc/
βββ app/
β βββ api/
β β βββ power-usage/route.ts # Power data endpoint (2s delay)
β β βββ grid-events/
β β βββ route.ts # Paginated events endpoint
β β βββ metadata/route.ts # Pagination metadata
β βββ layout.tsx # Root layout with Providers
β βββ page.tsx # Home: queries + components
β βββ providers.tsx # React Query + IndexedDB setup
β βββ globals.css
βββ components/
β βββ ui/ # shadcn/ui components
β βββ cache-status-indicator.tsx # Real-time cache status
β βββ power-usage-chart.tsx # Recharts line chart
β βββ events-table.tsx # Paginated table with skeletons
βββ lib/
βββ constants.ts # Mock data generation
βββ persister.ts # IndexedDB persister
βββ utils.ts # cn() utility
<PersistQueryClientProvider
client={queryClient}
persistOptions={{ persister: createIDBPersister() }}
onSuccess={() => queryClient.resumePausedMutations()}
>- gcTime: 24 hours (when cache expires)
- staleTime: 0 (always revalidate on access)
- Persister: IndexedDB via
idb-keyval
const eventsQuery = useQuery({
queryKey: ["grid-events", page],
queryFn: async () => {
const res = await fetch(`/api/grid-events?page=${page}`);
return res.json();
},
staleTime: 0, // Revalidate every time
});Each page is cached separately by query key, enabling instant navigation.
Pagination metadata is fetched independently from data:
/api/grid-events/metadataβ{ totalCount, totalPages, pageSize }- Cached for 1 hour (rarely changes)
- Enables accurate pagination rendering before data loads
- Prevents layout shift from unknown page counts
Problem: Variable table row counts and disappearing pagination cause layout jumps.
Solution:
- Table always renders exactly 5 rows (padding with invisible placeholders)
- Pagination always visible (no conditional rendering)
- Metadata loaded separately (totalPages known before data)
- Skeleton states match final content dimensions
Problem: Server renders without cache, client hydrates with cached data from IndexedDB.
Solution:
const [isHydrated, setIsHydrated] = useState(false);
useEffect(() => setIsHydrated(true), []);
// Show consistent state until hydrated
if (!isHydrated) {
return <LoadingSkeleton />;
}API routes include a delay parameter for realistic testing:
// Default 2s delay
fetch("/api/grid-events?page=1");
// No delay for testing
fetch("/api/grid-events?delay=0");Data is persisted to IndexedDB at:
Application -> IndexedDB -> keyval-store -> reactQuery
Inspect the cached data in Chrome DevTools -> Application -> IndexedDB.