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#

Program.cs
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#

  1. If the entry has data, it renders instantly (IsCachedData is true) — no loading flash.
  2. Staleness decides whether to refetch in the background: with the site's DefaultStaleTime of 10 seconds, data younger than that is served with no request at all.
  3. Concurrent same-key executions — from any component — join one in-flight fetch.
  4. 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.

One key, one type

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.

Blazor Server: Scoped is not negotiable

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#

Instant render on remount

Remountable component renders: 4

State IsLoading IsReFetching IsError IsCachedData IsStale ReFetchCount: 1UpdatedAt: 20:32:37
  • 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;
}