enduring
Description: If true the cache entry persists throughout automatic cache cleanups after the DataClient cache has reached the maximal amount of entries.
Default Value: false.
- How To: Only use
enduring: truefor global, critical data that must always be available and should not be evicted from the cache, even when the cache reaches its maximum capacity. Examples include application configuration, current user details, or feature flags.// Correct: Critical app data that should never be evicteduseFetch('app-config',fetchAppConfig,{ ttl: 30 * 60 * 1000, enduring: true } // Still has a TTL for staleness, but won't be evicted);// Correct: Translations or messages that are used globallyuseFetch('translations',fetchTranslations,{ ttl: -1, enduring: true } // Never expires, never evicted); - Best Practice: Do NOT overuse
enduring: true. Setting it on unbounded data (e.g., product details for every product a user views) prevents cache cleanup and can lead to severe memory leaks, eventually slowing down or crashing the application.// Incorrect: Enduring on unbounded data leads to memory leaksfunction ProductPage({ productId }) {const { data } = useFetch(['product', productId], // Many products, each with a unique ID() => fetchProduct(productId),{ enduring: true } // DANGER! Cache will grow indefinitely});// ...}
Deep Dive: How enduring works step by step
Example:
// Enduring entry
function AppConfig() {
const { data } = useFetch(
'app-config', // key
() => fetchAppConfig(), // action function
{ ttl: -1, enduring: true } // options
);
return <ConfigDisplay config={data} />;
}
// Regular entry
function ProductPage({ productId }) {
const { data } = useFetch(
['product', productId],
() => fetchProduct(productId),
{
ttl: 10 * 60 * 1000,
enduring: false // Default - can be evicted
}
);
return <ProductDisplay product={data} />;
}
What happens step by step with cache cleanup:
Scenario: Cache has reached maximum capacity
-
Initial state - Cache has 100 entries:
- 3 enduring entries:
app-config,current-user,feature-flags - 97 regular entries: Various product pages, search results, etc.
- 3 enduring entries:
-
User browses 20 more product pages:
- Cache now needs space for new entries
- Cache is full (120 entries, max 100)
-
DataClient triggers capacity cleanup:
- Step 1: Identify enduring entries → 3 entries marked as protected
- Step 2: Calculate entries to remove → Need to free 20 slots
- Step 3: Sort regular entries by "least recently used" (LRU)
- Step 4: Remove 20 oldest regular entries
- Enduring entries NEVER touched → Always safe
-
Result:
app-config: Still in cache (enduring)current-user: Still in cache (enduring)feature-flags: Still in cache (enduring)- Old product pages: Removed (least recently used)
- Recent product pages: Still in cache
What happens step by step with TTL expiration:
-
t=0min: Data cached
app-config: TTL = -1, enduring = trueproduct-123: TTL = 10min, enduring = false
-
t=11min: Cache validation runs
- Checks
app-config: TTL = -1 → Never expires → Kept in cache (enduring) - Checks
product-123: TTL expired (11min > 10min)- If
enduring: true→ Kept as stale (for next access to trigger refetch) - If
enduring: false→ Removed from cache
- If
- Checks
-
t=11min: User accesses product-123 again
- If entry was kept (enduring): Sees stale data → Triggers refetch → Smooth UX
- If entry was removed (!enduring): No cache → Fresh fetch → Slight delay
Memory leak scenario - What happens step by step:
// BAD: Enduring on unbounded data
function ProductPage({ productId }) {
const { data } = useFetch(
['product', productId], // productId can be 1, 2, 3, ... 10000
() => fetchProduct(productId),
{ enduring: true } // DANGER!
);
}
Timeline of memory leak:
- User views product 1 → Cache entry
['product', '1']created, enduring - User views product 2 → Cache entry
['product', '2']created, enduring - ...user browses 100 products...
- Cache now has 100 enduring product entries
- Cleanup tries to free memory → Can't remove enduring entries!
- User views product 101 → Cache forced to grow beyond limit
- Memory usage keeps increasing → Eventually slows down browser
- Cleanup is helpless → All entries are protected
GOOD: Enduring only for global data
function App() {
// Only 1 config entry - safe
const { data: config } = useFetch(
'app-config',
() => fetchAppConfig(),
{ enduring: true }
);
// Only 1 user entry - safe
const { data: user } = useFetch(
'current-user',
() => fetchCurrentUser(),
{ enduring: true }
);
}
function ProductPage({ productId }) {
// Many product entries - NOT enduring
const { data } = useFetch(
['product', productId],
() => fetchProduct(productId),
{
enduring: false // Can be evicted when needed
}
);
}
Key difference: Enduring protects from automatic cleanup but creates risk of memory leaks if overused. Use only for a small, fixed number of critical global entries.
⚙️ Configuration: DataClient Limits
To effectively use enduring without starving your cache of space for regular entries, it is crucial to configure the DataClient with appropriate limits.
Why it matters:
enduring: true entries occupy cache slots that cannot be cleared by the automatic garbage collector (LRU strategy). If your maxEntries limits are too low, a few enduring entries could consume a significant portion of your available cache, causing frequent evictions of non-enduring data (thrashing) or preventing new data from being cached efficiently.
Recommended Configuration:
Ensure your soft and hard limits are high enough to accommodate your expected number of enduring entries plus a healthy buffer for transient data.
// Example: Configuring DataClient in your application entry point
new DataClient({
cacheCheckInterval: 1800000, // Check for cleanup every 30 minutes
refetch: 14400000, // Default refetch interval (4 hours)
cache: new DataCache({
ttl: 14400000, // Default TTL (4 hours)
maxEntries: {
soft: 500, // Start cleaning up when we reach 500 entries
hard: 1000 // Absolute limit
}
})
});
Key Settings:
maxEntries.soft(500): The target size after cleanup. If you have 100 enduring entries, you effectively have 400 slots for regular data.maxEntries.hard(1000): The maximum capacity before aggressive cleanup occurs.cacheCheckInterval: How frequently the client checks if limits are exceeded or TTLs expired.