Caching
netron-browser ships an LRU cache + CacheMiddleware; netron-react layers a query cache on top with React-aware subscriptions.
Two cache layers
| Layer | When to use |
|---|---|
| netron-browser LRU | Vanilla JS / web workers / non-React clients |
| netron-react QueryCache | React apps — used by useQuery |
Both share the same TTL / stale-while-revalidate / tag invalidation semantics. Pick one — don't double-cache.
netron-react QueryCache
Used automatically by every useQuery / useService(...).x.useQuery:
const { data, isLoading, isStale } = users.getUser.useQuery([userId], {
staleTime: 30_000,
gcTime: 5 * 60_000,
refetchOnWindowFocus: true,
refetchOnReconnect: true,
refetchInterval: 60_000,
});
| Option | Default | Meaning |
|---|---|---|
staleTime | 0 | Data is fresh for N ms; while fresh, no refetch on subscribe |
gcTime | 5 * 60_000 | Drop from cache N ms after last subscriber unmounts |
refetchOnMount | true | Refetch when a component mounts subscribing to this key |
refetchOnWindowFocus | false | Refetch when tab regains focus |
refetchOnReconnect | true | Refetch when WS / network reconnects |
refetchInterval | false | Periodic refetch in ms |
enabled | true | Skip the query when false |
placeholderData | — | Initial value before first fetch |
select | — | Transform data per subscription |
Cache key
Generated from [service, method, args]:
// All these are the same cache key:
users.getUser.useQuery(['u_42']);
users.getUser.useQuery(['u_42']);
// These are different keys:
users.getUser.useQuery(['u_42']);
users.getUser.useQuery(['u_43']);
Args are deep-equal-compared — {a:1, b:2} and {b:2, a:1}
match.
Programmatic cache access
import { useNetronClient } from '@omnitron-dev/netron-react';
function CacheManager() {
const client = useNetronClient();
const cache = client.getQueryCache();
return (
<>
<Button onClick={() => cache.invalidateQueries(['users'])}>
Refresh all users queries
</Button>
<Button onClick={() => cache.removeQueries(['users', 'getUser', 'u_42'])}>
Drop one specific
</Button>
<Button onClick={() => cache.setQueryData(['users', 'getUser', 'u_42'], updatedUser)}>
Patch cached value
</Button>
<Button onClick={() => cache.clear()}>
Nuke everything
</Button>
</>
);
}
| Method | Effect |
|---|---|
getQueryData(key) | Read cached data without subscribing |
setQueryData(key, data) | Write to cache directly |
setQueryData(key, (old) => new) | Functional update |
invalidateQueries(filter) | Mark stale → subscribers refetch |
removeQueries(filter) | Drop from cache |
cancelQueries(filter) | Abort in-flight queries matching filter |
prefetchQuery(args) | Warm cache without rendering |
clear() | Drop everything |
Filter patterns
cache.invalidateQueries(['users']); // every users.* query
cache.invalidateQueries(['users', 'getUser']); // every getUser
cache.invalidateQueries(['users', 'getUser', 'u_42']); // one specific
cache.invalidateQueries({ predicate: (q) => q.queryKey[0] === 'users' && q.state.dataUpdatedAt < Date.now() - 60_000 });
The functional predicate form lets you invalidate by age,
state, or any custom condition.
Stale-while-revalidate
users.getUser.useQuery([userId], {
staleTime: 30_000,
gcTime: 5 * 60_000,
});
Flow when component mounts:
isStale is exposed in the result — components can show a
subtle "refreshing" indicator while stale-while-revalidate
runs.
Optimistic updates
const updateProfile = users.updateProfile.useMutation({
onMutate: async (newProfile) => {
// 1. Cancel any in-flight queries that would clobber the optimistic update
await cache.cancelQueries(['users', 'getUser', userId]);
// 2. Snapshot the previous value
const previous = cache.getQueryData(['users', 'getUser', userId]);
// 3. Optimistically update
cache.setQueryData(['users', 'getUser', userId], (old) => ({
...old,
...newProfile,
}));
// 4. Return context for rollback
return { previous };
},
onError: (err, _newProfile, context) => {
// Rollback to snapshot
if (context?.previous) {
cache.setQueryData(['users', 'getUser', userId], context.previous);
}
},
onSettled: () => {
// Always re-fetch authoritative state
cache.invalidateQueries(['users', 'getUser', userId]);
},
});
Pattern:
- Cancel to avoid race with in-flight fetches.
- Snapshot for rollback.
- Optimistic write for instant UI feedback.
- Rollback on error — restore snapshot.
- Invalidate on settled — re-sync with server truth.
Tag-based invalidation
For non-React (netron-browser LRU):
import { LRUCache, CacheMiddleware } from '@omnitron-dev/netron-browser';
const cache = new LRUCache({
maxSize: 1_000,
defaultTTL: 60_000,
staleWhileRevalidate: 10_000,
});
client.use(CacheMiddleware({ cache }));
// Tag queries with arbitrary labels:
await client
.cache({ tags: ['user:u_42', 'tier:pro'] })
.service<UserService>('users')
.getUser('u_42');
// Later, when user u_42 changes:
cache.invalidateByTag('user:u_42');
// Or all pro-tier cached data:
cache.invalidateByTag('tier:pro');
Tags are arbitrary strings — typical patterns:
user:<id>— invalidate one user's queriestenant:<id>— invalidate one tenant's queriesentity:<type>— invalidate one collectionfeature:<flag>— invalidate when a flag flips
A single cache entry can carry multiple tags; invalidating any one drops the entry.
Prefetching
import { useNetronClient } from '@omnitron-dev/netron-react';
function ProjectLink({ id }: { id: string }) {
const client = useNetronClient();
return (
<Link
to={`/projects/${id}`}
onMouseEnter={() => {
client.getQueryCache().prefetchQuery({
service: 'projects',
method: 'getProject',
args: [id],
});
}}
>
{name}
</Link>
);
}
When the user hovers a link, prefetch the target's data so the destination renders instantly.
Cache stats
const stats = cache.getStats();
// { size, hits, misses, evictions, hitRatio }
Surface in devtools or a metrics dashboard.
React Query parity
netron-react's QueryCache API is intentionally TanStack Query–compatible at the high level — if you've used React Query, the mental model carries over. The differences:
- Cache key is
[service, method, args]not free-form. - Backed by netron-browser, not raw fetch.
- Subscribes via
useService/useQueryhooks; same shape.
Best practices
staleTime≥ 30 s for most read queries — saves needless refetches.refetchOnWindowFocus: truefor dashboards and admin surfaces — keeps data fresh after returning to a tab.- Optimistic updates for fast-feedback mutations — but always invalidate on settled to converge with server truth.
- Invalidate by widest sensible key after mutations —
invalidating
['users']after creating a user catcheslistcount+getUsercallers.
- Use tags for non-React clients — easier to reason about than key patterns.
- Don't cache mutating calls.
delete/updateshould never read from cache.
Anti-patterns
staleTime: 0everywhere. Defeats caching; every mount refetches.gcTime: Infinity. Cache grows unboundedly; first call after page reload is cold anyway.- Manual cache writes from multiple components. Use invalidation; manual writes drift.
- Caching paginated lists without page-aware keys.
[users, list, {page: 1}]and[users, list, {page: 2}]must be separate keys. - Invalidating after every keystroke. Debounce or move to
onBlur.
See also
- Middleware —
CacheMiddlewareconfiguration - netron-react —
useQuery/useMutation - Multi-backend — per-backend cache