Quick start

From zero to a cached, cancellable, auto-rendering query in three steps.

1. Declare a query and execute it#

Todos.razor
@page "/todos"
@inherits DejaComponentBase
@inject TodoApi Api

@if (_todos.IsLoading && _todos.Data is null) { <p>Loading…</p> }
@if (_todos.IsError) { <p class="error">@_todos.ErrorMessage</p> }

<ul>
    @foreach (var todo in _todos.Data ?? [])
    {
        <li>@todo.Title</li>
    }
</ul>

@code {
    private readonly Query<List<Todo>> _todos = new();

    protected override Task OnInitializedAsync()
        => _todos.Execute("todos", Api.GetTodosAsync);
}

Execute("todos", Api.GetTodosAsync) is the keyed shorthand: the first argument is the cache key (a string converts implicitly to QueryKey.Of("todos")), the second is any Func<CancellationToken, Task<T>>. Deja passes a token that is cancelled when a newer load supersedes this one or the component is disposed.

2. Bind the state#

Everything the request lifecycle exposes is a bindable property:

  • IsLoading — any execution in flight
  • IsReFetching — an execution after the first; use for subtle refresh indicators instead of a full-page spinner
  • IsError / ErrorMessage — the most recent failure
  • Data — the most recent result
  • IsCachedData / IsStale / UpdatedAt — cache facts for keyed queries

3. Write with a mutation, invalidate the key#

Todos.razor (continued)
<input @bind="_title"/>
<button @onclick="Add" disabled="@_addTodo.IsLoading">Add</button>

@code {
    private readonly Mutation<Todo> _addTodo = new();
    private string _title = "";

    private Task Add() => _addTodo.Execute(
        token => Api.AddTodoAsync(_title, token),
        p => p.InvalidateKeys = [QueryKey.Of("todos")]);
}

On success, every mounted query whose key starts with "todos" refetches in the background — in every component showing that data. No event wiring, no manual reload calls.

See it live#

That's the whole loop. The todo list demo is exactly this pattern running against a live API, with a state inspector attached. From here: