Abbruch

Deja bindet jede Ausführung an die Lebensdauer der besitzenden Komponente und bricht verdrängte Ladevorgänge automatisch ab — meist ganz ohne Verdrahtung.

Der implizite Token#

Innerhalb einer DejaComponentBase-Komponente verwendet jede Query- und Mutation-Ausführung standardmäßig den Lebensdauer-Token der Komponente. Beim Wegnavigieren wird die Komponente disposed, das bricht den Token ab, und das bricht den Request ab — bis hinunter zum Server. Deine Fetch-Funktion muss den Token nur durchreichen:

// Nothing to wire: the token is the component's lifetime token.
protected override Task OnInitializedAsync()
    => _todos.Execute("todos", token => Api.GetTodosAsync(token));

Ein Fetch, der Abbruch nicht beobachten kann, ignoriert den Token per Discard: QueryFunction = _ => LegacyFetchAsync(). Deja verwirft sein veraltetes Ergebnis trotzdem, wenn ein neuerer Ladevorgang übernommen hat — nur den Request auf der Leitung kann es dann nicht abbrechen.

Ein Token pro Aufruf ersetzt den impliziten#

Wer CancellationToken auf den Parametern setzt, bindet eine Ausführung an etwas Engeres als die Komponente — etwa die eigene Source eines Suchfelds. Der Token ersetzt den Komponenten-Token; er wird nicht mit ihm kombiniert:

_search.Execute(token => Api.SearchAsync(term, token), p =>
{
    p.CancellationToken = _searchCts.Token;   // replaces, not combines
});

Verdrängen und Abbrechen (ungecachter Pfad)#

Ein Execute ohne Key bricht die vorige laufende Ausführung ab, bevor es startet. Der ältere Request wird abgebrochen, seine Callbacks werden übersprungen, und sein verspätetes Ergebnis — selbst aus einem Fetch, der den Token ignoriert hat — kann die Daten des neueren Ladevorgangs nie überschreiben. Abbruch ist kein Fehler: kein Fehlerzustand, keine Callbacks; der verdrängende Ladevorgang treibt das nächste Update.

Der subtile Fall: Abbruch auf dem gecachten Pfad#

Ein Fetch mit Key gehört zum Cache-Eintrag, und andere Komponenten warten womöglich darauf. Der Token eines Aufrufers wird deshalb bewusst nicht in den gemeinsamen Fetch verknüpft: Ihn abzubrechen löst diesen Aufrufer ab, ohne den Request abzubrechen. Das Unmounten einer Komponente darf nicht die Daten einer anderen abbrechen. Der gemeinsame Fetch selbst wird erst abgebrochen, wenn der letzte Subscriber den Eintrag verlässt.

ComponentToken für eigene Aufrufe#

Arbeit, die Deja nicht für dich ausführt — ein direkter API-Ping aus einem Event-Handler — kann trotzdem die Lebensdauer der Komponente teilen:

private async Task OnClick()
    => await Api.PingAsync(ComponentToken);

Live-Demo#

Verdrängen und Abbrechen
Unkeyed query IsLoading IsReFetching IsError IsCachedData IsStale ReFetchCount: 0

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 = _ => { });
    }
}