Skip to content

TanStack Query Persistence

This document serves as a critical reference for configuring TanStack Query (v5) with Persistence in the ChatKcal codebase. It addresses specific technical limitations discovered during implementation that affect how long data can be safely retained.


1. The Core Configuration Rule

To ensure a robust "Offline History" experience (e.g., retaining data for 30 days) while avoiding bugs, you must adhere to the following configuration pattern:

Setting Location Recommended Value Reason
staleTime useQuery options 10 Seconds (e.g., 1000 * 10) Keeps data "fresh" to avoid unnecessary refetches during rapid navigation, but allows multi-device sync on focus.
maxAge PersistQueryClientProvider props 30 Days (e.g., 1000 * 60 * 60 * 24 * 30) Controls how long data is stored on Disk (IndexedDB). Mathematical check, safe for large values.
gcTime QueryClient defaults (or Hook) Infinity Controls how long inactive data stays in Memory. Must be Infinity to avoid setTimeout overflow.

2. The Nuance: staleTime vs gcTime

It is crucial to understand the difference to avoid over-fetching:

  • staleTime (Freshness): "How long until I should ask the server for updates?"

    • Recommendation: 5 Minutes.
    • Why: ChatKcal is primarily single-player. If you log a meal, the cache is updated optimistically. You rarely need to pull data from the server unless you are switching devices. A 5-minute buffer makes navigating "Today -> Yesterday -> Today" instant and network-free.
    • Behavior: If you open the app after 2 minutes, it shows data instantly and does not fetch. If after 6 minutes, it shows data instantly (stale) and fetches in the background.
  • gcTime (Memory Retention): "How long until I delete this from RAM?"

    • Recommendation: Infinity.
    • Why: We want the data to stay in memory as long as the session is active to support instant navigation, relying on maxAge for long-term disk cleanup.

3. The Technical "Gotcha": setTimeout Overflow

The Problem

TanStack Query uses the browser's standard setTimeout function to schedule garbage collection for inactive queries.

  • Limit: setTimeout uses a 32-bit signed integer for the delay.
  • Max Delay: 2^31 - 1 milliseconds ≈ 24.8 days.
  • Behavior: If you set gcTime to a value larger than this limit (e.g., 30 days), the timer overflows. In many environments, this causes the callback to fire immediately (0ms delay), resulting in the instant removal of your cache as soon as you navigate away from a page.

The Solution

Use gcTime: Infinity for the in-memory cache.

  • This disables the garbage collection timer entirely.
  • Memory Risk? Low for our use case. Text-based meal data is small.
  • Persistence: The maxAge setting on the persister acts as the true expiry. When the app reloads, any data in IndexedDB older than 30 days will be discarded during hydration.

3. Code Example

frontend/src/main.jsx

const CACHE_TIME = 1000 * 60 * 60 * 24 * 30; // 30 Days (for Disk)

const queryClient = new QueryClient({
    defaultOptions: {
        queries: {
            // CRITICAL: Prevent in-memory GC from firing immediately due to overflow
            gcTime: Infinity,
        },
    },
});

const persister = {
    ...
}; // idb-keyval setup

// ...

<
PersistQueryClientProvider
client = {
    queryClient
}
persistOptions = {
        {
            persister,
            maxAge: CACHE_TIME // Safe to use 30 days here
        }
    } >
    <
    App / >
    <
    /PersistQueryClientProvider>

4. Exception: User Settings (staleTime: 0)

While 10 seconds or 5 minutes is standard for content (Meals), Configuration Data (like User Targets) requires a stricter strategy when combined with Persistence.

  • Problem: If staleTime is high (e.g., 1 hour), the app loads "stale" settings from disk and does not verify them with the server. If you changed settings on another device, the current device will fail to reflect those changes until the cache expires.
  • Solution: Set staleTime: 0 for configuration hooks.
    • Behavior: The app renders immediately using the cached data (from disk), preserving the "instant" feel.
    • Background Fetch: React Query simultaneously triggers a background fetch because the data is considered "stale" immediately.
    • Result: You get instant rendering plus guaranteed consistency with the server.

Code Example (useSettings.ts)

useQuery({
  queryKey: ['userTargets'],
  queryFn: fetchTargets,
  staleTime: 0, // Always fetch fresh settings on mount
  gcTime: Infinity
})

5. Summary Checklist

  • Never set gcTime > 24 days (approx 2 billion ms).
  • Always align maxAge with your desired business rule (e.g., 30 days).
  • Use Infinity for gcTime if your persistence duration exceeds 24 days.
  • Use staleTime: 0 for critical user settings to ensure cross-device consistency.