The cache
Register AddDeja() and every keyed query taps a shared, app-wide cache: instant
renders from cached data, one fetch per key no matter how many components ask, and background
revalidation when data goes stale.
Opting in#
builder.Services.AddDeja(options =>
{
options.DefaultStaleTime = TimeSpan.FromSeconds(10); // fresh window: no refetch on mount
options.DefaultCacheTime = TimeSpan.FromMinutes(5); // unsubscribed entry lifetime
});
Queries opt in per execution by setting a key; a query without
a key never touches the cache. Components inheriting DejaComponentBase get the
client handed to their queries automatically — outside a component, pass it via the
Query<T>(DejaClient) constructor or QueryParameters<T>.Client.
What a keyed Execute does#
- If the entry has data, it renders instantly (
IsCachedDatais true) — no loading flash. - Staleness decides whether to refetch in the background: with the site's
DefaultStaleTimeof 10 seconds, data younger than that is served with no request at all. - Concurrent same-key executions — from any component — join one in-flight fetch.
- When the result lands, the entry notifies every subscribed query, and each notifies its own component. One code path for all subscribers, so two components sharing a key cannot drift.
Entry lifecycle and eviction#
An entry with live subscribers is never evicted. When the last subscriber unmounts, the
eviction clock starts: the entry survives for the effective cache time (default
5 minutes), so a component remounting within the window gets its data instantly. An optional
MaxEntries cap evicts least-recently-used subscriber-less entries early.
Reading and writing directly#
DejaClient is also a manual surface: GetData /
TryGetData to peek, SetData to write (see the
optimistic write demo), InvalidateAsync to
mark stale and refetch, Remove / Clear to drop entries. The full
surface is in the API reference.
A key stores one data type. Reading with the wrong type (TryGetData) returns
false and logs a debug warning; executing a query against a key that already holds a
different type throws — two call sites disagreeing about a key's shape is a bug that
silent replacement would mask.
The client is registered Scoped: per browser tab on WebAssembly, per user circuit on Server. "Optimising" it to a singleton on Blazor Server would share one user's cached API responses with every other user.
Live demo#
Remountable component renders: 4
- delectus aut autem
- quis ut nam facilis et officia qui
- fugiat veniam minus
- et porro tempora
- laboriosam mollitia et enim quasi adipisci quia provident illum
| Key | Data | Fetching | Invalidated | Subscribers | Updated |
|---|---|---|---|---|---|
| ["todos","list",5] | ✓ | · | · | 1 | 20:32:37 |
Unmounting drops the subscriber count to zero, which starts the entry's eviction clock (5 minutes here). Remounting within the site's 10-second stale time serves the cache with no request at all; after that, it still renders instantly and refetches in the background.
<div class="demo-toolbar">
<button class="demo-button primary" @onclick="() => _mounted = !_mounted">
@(_mounted ? "Unmount component" : "Mount component")
</button>
</div>
@if (_mounted)
{
<SharedCachePanel Title="Remountable component"/>
}
else
{
<p class="demo-note">Component unmounted. The cache entry survives below — remount within the
cache time and the data renders instantly, then revalidates in the background if stale.</p>
}
<CacheInspector Keys="@(new[] { DocsKeys.TodoList(5) })"/>
<p class="demo-note">
Unmounting drops the subscriber count to zero, which starts the entry's eviction clock
(5 minutes here). Remounting within the site's 10-second stale time serves the cache with no
request at all; after that, it still renders instantly and refetches in the background.
</p>
@code {
private bool _mounted = true;
}