Caching
netron-browser caches HTTP responses through its fluent HTTP
interface (HttpCacheManager); netron-react layers a query
cache on top with React-aware subscriptions.
Two cache layers
| Layer | When to use |
|---|---|
netron-browser fluent cache (HttpCacheManager) | Vanilla JS / web workers / non-React HTTP 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.
The fluent cache is opt-in per call (
.cache(...)) on a service obtained viapeer.queryFluentInterface(...). The basiccreateClient()/HttpClientpath does not cache — itshttp.caching/http.cacheTTLoptions are currently inert.
netron-react QueryCache
Used automatically by every useQuery / useService(...).x.useQuery:
const { data, isLoading, isStale } = users.getUser.useQuery([userId], {
staleTime: 30_000,
cacheTime: 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 |
cacheTime | 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={() => client.invalidateQueries({ queryKey: ['users'] })}>
Refresh all users queries
</Button>
<Button onClick={() => client.removeQueries({ queryKey: ['users', 'getUser', 'u_42'] })}>
Drop one specific
</Button>
<Button onClick={() => cache.set(['users', 'getUser', 'u_42'], updatedUser)}>
Patch cached value
</Button>
<Button onClick={() => client.clear()}>
Nuke everything
</Button>
</>
);
}
The TanStack-style filter methods live on the client; the
low-level read/write methods live on the QueryCache
(client.getQueryCache()):
| Method | Lives on | Effect |
|---|---|---|
getQueryCache().get(key) | cache | Read cached data without subscribing |
getQueryCache().set(key, data) | cache | Write to cache directly |
invalidateQueries(filters) | client | Mark stale → subscribers refetch |
removeQueries(filters) | client | Drop from cache |
cancelQueries(filters) | client | Abort in-flight queries matching filter |
prefetchQuery(key, fetcher) | client | Warm cache without rendering |
clear() | client | Drop everything |
To patch a cached value functionally, read-then-set:
const prev = cache.get(['users', 'getUser', 'u_42']);
cache.set(['users', 'getUser', 'u_42'], { ...prev, ...patch });
Filter patterns
The client's filter methods take a QueryFilters object —
{ queryKey?, exact?, status?, stale?, fetching?, predicate? }.
queryKey is a prefix match by default; add exact: true for
an exact match.
client.invalidateQueries({ queryKey: ['users'] }); // every users.* query
client.invalidateQueries({ queryKey: ['users', 'getUser'] }); // every getUser
client.invalidateQueries({ queryKey: ['users', 'getUser', 'u_42'], exact: true }); // one specific
client.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,
cacheTime: 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
client.cancelQueries({ queryKey: ['users', 'getUser', userId] });
// 2. Snapshot the previous value
const previous = cache.get(['users', 'getUser', userId]);
// 3. Optimistically update
cache.set(['users', 'getUser', userId], {
...previous,
...newProfile,
});
// 4. Return context for rollback
return { previous };
},
onError: (err, _newProfile, context) => {
// Rollback to snapshot
if (context?.previous) {
cache.set(['users', 'getUser', userId], context.previous);
}
},
onSettled: () => {
// Always re-fetch authoritative state
client.invalidateQueries({ queryKey: ['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 HTTP clients, attach an HttpCacheManager to the
peer and tag calls through the fluent .cache({ tags }):
import { HttpCacheManager } from '@omnitron-dev/netron-browser';
const cache = new HttpCacheManager({ maxEntries: 1_000 });
peer.setCacheManager(cache);
const users = await peer.queryFluentInterface<UserService>('users@1.0.0');
// Tag a cached call with arbitrary labels:
await users
.cache({
maxAge: 60_000,
staleWhileRevalidate: 10_000,
tags: ['user:u_42', 'tier:pro'],
})
.api.getUser('u_42');
// Later, when user u_42 changes — invalidate by tag.
// (The array form of invalidate() matches tags; a bare string
// matches cache keys, so pass tags as an array.)
cache.invalidate(['user:u_42']);
// Or all pro-tier cached data:
cache.invalidate(['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. cache.invalidate() also accepts a key
RegExp or a 'prefix*' string for pattern-based invalidation.
Prefetching
import { useNetronClient, useService } from '@omnitron-dev/netron-react';
function ProjectLink({ id }: { id: string }) {
const client = useNetronClient();
const projects = useService<ProjectService>('projects');
return (
<Link
to={`/projects/${id}`}
onMouseEnter={() => {
client.prefetchQuery(
['projects', 'getProject', id],
() => projects.getProject.call(id),
);
}}
>
{name}
</Link>
);
}
When the user hovers a link, prefetch the target's data so the destination renders instantly.
Cache stats
The fluent HttpCacheManager exposes hit/miss statistics:
const stats = cache.getStats();
// CacheStats:
// { entries, hits, misses, hitRate, sizeBytes, activeRevalidations }
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.cacheTime: 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
- Browser client — fluent
HttpCacheManager/.cache() - netron-react —
useQuery/useMutation - Multi-backend — shared cache across backends