Cancellation
Deja scopes every execution to the owning component's lifetime and cancels superseded loads automatically — usually with nothing to wire.
The ambient token#
Inside a DejaComponentBase component, every query and mutation execution uses the
component's lifetime token by default. Navigating away disposes the component, which cancels
the token, which aborts the request — down to the server. Your fetch function just has to pass
the token through:
// Nothing to wire: the token is the component's lifetime token.
protected override Task OnInitializedAsync()
=> _todos.Execute("todos", token => Api.GetTodosAsync(token));
A fetch that can't observe cancellation ignores the token with a discard:
QueryFunction = _ => LegacyFetchAsync(). Deja still discards its stale result
if a newer load has taken over — it just can't abort the wire request.
A per-call token replaces the ambient one#
Setting CancellationToken on the parameters scopes an execution to something
narrower than the component — a search box's own source, say. It replaces the
component token; it does not combine with it:
_search.Execute(token => Api.SearchAsync(term, token), p =>
{
p.CancellationToken = _searchCts.Token; // replaces, not combines
});Supersede-and-cancel (uncached path)#
An unkeyed Execute cancels the previous in-flight execution before starting. The
older request is aborted, its callbacks are skipped, and its late result — even from a fetch
that ignored the token — can never overwrite the newer load's data. Cancellation is not an
error: no error state, no callbacks; the superseding load drives the next update.
The subtle case: cancelling on the cached path#
A keyed fetch belongs to the cache entry, and other components may be waiting on it. So a caller's token is deliberately not linked into the shared fetch: cancelling it detaches that caller without cancelling the request. One component unmounting must not abort another component's data. The shared fetch itself is only cancelled when the last subscriber leaves the entry.
ComponentToken for your own calls#
Work Deja doesn't run for you — a direct API ping from an event handler — can still share the component's lifetime:
private async Task OnClick()
=> await Api.PingAsync(ComponentToken);Live demo#
Last request started: #0
No result yet.
Raise the latency in the demo controls, then click the start button rapidly. Every click supersedes — and cancels — the in-flight request, so the result on screen is always the latest click, never a slow stale response arriving late. Navigating away cancels via the component's lifetime token with zero wiring.
@inherits DejaComponentBase
@inject JsonPlaceholderApi Api
<div class="demo-toolbar">
<button class="demo-button primary" @onclick="LoadNext">
Start request #@(_requested + 1)
</button>
<button class="demo-button danger" @onclick="() => _todo.ClearData(cancelCurrentRequest: true)">
ClearData(cancel: true)
</button>
</div>
<StateInspector Query="_todo" Label="Unkeyed query"/>
<div class="demo-panel">
<h4>Last request started: #@_requested</h4>
@if (_todo.Data is { } todo)
{
<p>Result on screen: <strong>#@todo.Id</strong> — @todo.Title</p>
}
else
{
<p class="demo-note">No result yet.</p>
}
</div>
<p class="demo-note">
Raise the latency in the demo controls, then click the start button rapidly. Every click
supersedes — and cancels — the in-flight request, so the result on screen is always the
<em>latest</em> click, never a slow stale response arriving late. Navigating away cancels via
the component's lifetime token with zero wiring.
</p>
@code {
// Unkeyed on purpose: the supersede-and-cancel behaviour belongs to the uncached path.
private readonly Query<TodoDto> _todo = new();
private int _requested;
private Task LoadNext()
{
var id = ++_requested;
return _todo.Execute(token => Api.GetTodoAsync(id, token), p => p.OnError = _ => { });
}
}