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
maxAgefor 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:
setTimeoutuses a 32-bit signed integer for the delay. - Max Delay:
2^31 - 1milliseconds ≈ 24.8 days. - Behavior: If you set
gcTimeto 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
maxAgesetting 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
staleTimeis 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: 0for 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
maxAgewith your desired business rule (e.g., 30 days). - Use
InfinityforgcTimeif your persistence duration exceeds 24 days. - Use
staleTime: 0for critical user settings to ensure cross-device consistency.