Queries
A Query<T> tracks a single asynchronous read and exposes its lifecycle as
bindable state. This guide covers the three ways to run one and the parameters that shape an
execution.
Three Execute overloads#
Keyed shorthand — the common case. The key opts the query into the shared cache; anything else is set through the optional configure hook:
private readonly Query<List<Todo>> _todos = new();
protected override Task OnInitializedAsync()
=> _todos.Execute("todos", Api.GetTodosAsync, p =>
{
p.StaleTime = TimeSpan.FromSeconds(30);
p.OnError = e => Log.Warn(e);
});Unkeyed shorthand — a fetch that never touches the cache. A newer call supersedes (and cancels) an older in-flight one, which is exactly what a type-ahead search wants:
// Each keystroke: the previous in-flight search is cancelled, the newest wins.
private Task OnSearchChanged(string term)
=> _results.Execute(token => Api.SearchAsync(term, token));Full form — everything explicit, for when the call site builds parameters programmatically:
await _todos.Execute(new QueryParameters<List<Todo>>
{
QueryKey = QueryKey.Of("todos", "list", page),
QueryFunction = token => Api.GetTodosAsync(page, token),
PlaceholderData = [],
Select = todos => [.. todos.OrderBy(t => t.Title)],
});The state surface#
IsLoading— true while any execution is loading.IsReFetching— true while an execution after the first is loading; drive subtle refresh indicators with this and keep the data on screen.IsError/ErrorMessage— the most recent failure; cleared by a successful refetch.Data— the most recent result.ReFetchCount— how many times the query has executed.UpdatedAt— when the current data was fetched (cached path only).IsCachedData— true whileDatacame from the cache and no fresh fetch has completed during this query's subscription.IsStale— true when the observed cache entry is invalidated or older than the effective stale time (always false on the uncached path).
Enabled — dependent queries#
Enabled = false serves cached data but never fetches — for queries whose inputs
aren't ready yet (no user selected, a filter still empty). null, the default,
means enabled. When the input arrives, execute again with Enabled = true.
Cached path only.
Chaining dependent requests#
When the second request's key or arguments come from the first result, run it from
OnSuccessAsync. The callback is awaited, so each step waits for the one before it
and the outer Execute only completes once the whole chain has — which means an
ordinary if is enough to branch on what the previous step returned.
await _user.Execute("user", Api.GetUserAsync, p => p.OnSuccessAsync = async user =>
{
if (user is null) return;
await _orders.Execute(QueryKey.Of("orders", user.Id), t => Api.GetOrdersAsync(user.Id, t),
o => o.OnError = e => Log.Warn(e));
// Each step sees the previous one settled
if (_orders.Data is { Count: > 0 })
{
await _invoices.Execute(QueryKey.Of("invoices", user.Id), t => Api.GetInvoicesAsync(user.Id, t));
}
});
Callbacks run on every completed Execute, including one that served fresh cached
data without fetching, so a chain behaves the same on a cache hit as on a fetch. (A query that
is Enabled = false over an empty cache has no result to hand over, so it settles
without reporting success.) The same pattern works from a mutation — see
Mutations.
Fan-out: a write, two parallel reads, then a third#
The interesting shape is not a straight line. Here a real write runs first; on success two
queries load in parallel; and when both have settled, a fourth request runs off their
combined result. The rule that makes this readable: awaiting is what orders the
steps, and each Execute only ever writes its own query's state.
// 1 — a real write. Everything below runs only if it succeeds.
await _createTodo.Execute(t => Api.CreateTodoAsync(draft, t), p =>
{
p.OnError = e => _error = e.Message;
p.OnSuccessAsync = async created =>
{
// 2a + 2b — started, not awaited: both requests go out together.
var author = _author.Execute(QueryKey.Of("users", "detail", created!.UserId),
t => Api.GetUserAsync(created.UserId, t),
o => o.OnError = e => _error = $"author: {e.Message}");
var todos = _authorTodos.Execute(QueryKey.Of("todos", "list", "user", created.UserId),
t => Api.GetTodosByUserAsync(created.UserId, t),
o => o.OnError = e => _error = $"todos: {e.Message}");
// Each query drives its own IsLoading/Data/IsError, so these two never
// interfere — whichever finishes first clears only its own flags.
await Task.WhenAll(author, todos);
// 3 — runs after both, and can branch on their combined result.
if (_author.Data is { } user && _authorTodos.Data is { Count: > 0 })
{
await _posts.Execute(QueryKey.Of("posts", "list", user.Id),
t => Api.GetPostsAsync(user.Id, t),
o => o.OnError = e => _error = $"posts: {e.Message}");
}
};
});
Read the ordering off the awaits. Step 1's OnSuccessAsync is awaited
by the mutation, so nothing downstream starts until the write has actually succeeded. Steps 2a
and 2b are started before either is awaited — that is the whole trick — so both requests
are on the wire at once and the pair costs one round trip rather than two.
Task.WhenAll then waits for the slower of the two. Step 3 sits after that await, so
it sees both results settled and can be gated on them with a plain if.
Because each query owns its own state, the parallel pair cannot interfere with each other:
_author.IsLoading goes out when the author request finishes, whether or not the
todos request is still running, and each writes only its own Data,
IsError and ErrorMessage. Two spinners tied to two queries resolve
independently, and a failure in 2b leaves 2a's data on screen untouched. That independence is
also why each step carries its own error callback — a failure is then attributed to the request
that caused it instead of surfacing on the parent.
The loading flags nest rather than compete. The mutation stays IsLoading for the
entire chain — its Execute has not returned yet — while each query's flag covers
only its own request. So bind a global "working" indicator to the mutation, and per-row
spinners to the individual queries.
Invalidating a parent key refetches that query and re-renders, but it does not re-run the
callback chain — the dependent query keeps its previous data. Chains re-run when you call
Execute. Invalidate the dependent keys too if they need to follow.
Two smaller things on long chains: give every step its own error callback, or a child's
failure surfaces on the parent (an unobserved failure throws, and that exception propagates
out of the parent's OnSuccessAsync); and the parent stays
IsLoading for the whole chain, so bind spinners to the last query or to the
combination.
Step 1 writes. Steps 2a and 2b start together once the write succeeds and load independently —
watch their two IsLoading badges light up and go out on their own. Step 3 only
starts after both have settled.
@inherits DejaComponentBase
@inject JsonPlaceholderApi Api
<div class="demo-toolbar">
<input class="demo-input" style="flex:1" placeholder="Todo title" @bind="_title" @bind:event="oninput"/>
<button class="demo-button primary" @onclick="Run"
disabled="@(_create.IsLoading || string.IsNullOrWhiteSpace(_title))">
@(_create.IsLoading ? "Running chain…" : "Create + load")
</button>
</div>
<p class="demo-note">
Step 1 writes. Steps 2a and 2b start together once the write succeeds and load independently —
watch their two <code>IsLoading</code> badges light up and go out on their own. Step 3 only
starts after both have settled.
</p>
<MutationInspector Mutation="_create" Label="1 · Create todo (write)"/>
<StateInspector Query="_author" Label="2a · Author (parallel)"/>
<StateInspector Query="_authorTodos" Label="2b · Author's todos (parallel)"/>
<StateInspector Query="_posts" Label="3 · Posts (after both)"/>
@if (_stepLog.Count > 0)
{
<div class="demo-panel">
<h4>Chain timeline</h4>
<ol class="demo-list">
@foreach (var step in _stepLog)
{
<li>@step</li>
}
</ol>
</div>
}
@if (_error is not null)
{
<div class="demo-error">@_error</div>
}
@code {
private readonly Mutation<TodoDto> _create = new();
private readonly Query<UserDto> _author = new();
private readonly Query<IReadOnlyList<TodoDto>> _authorTodos = new();
private readonly Query<IReadOnlyList<PostDto>> _posts = new();
private readonly List<string> _stepLog = [];
private string _title = string.Empty;
private string? _error;
private const int AuthorId = 1;
private Task Run()
{
_stepLog.Clear();
_error = null;
return _create.Execute(
token => Api.CreateTodoAsync(new NewTodo(AuthorId, _title.Trim(), false), token),
p =>
{
p.OnSuccessAsync = async created =>
{
Log($"1 · created todo #{created!.Id} for user {created.UserId}");
_title = string.Empty;
// Both start before either is awaited, so they overlap on the wire. Each writes
// only its own query's state, so neither can disturb the other's flags.
var author = _author.Execute(
DocsKeys.User(created.UserId),
t => Api.GetUserAsync(created.UserId, t),
o => o.OnError = Fail("2a"));
var authorTodos = _authorTodos.Execute(
DocsKeys.TodosByUser(created.UserId),
t => Api.GetTodosByUserAsync(created.UserId, t),
o => o.OnError = Fail("2b"));
await Task.WhenAll(author, authorTodos);
Log($"2 · both settled — author {_author.Data?.Name}, {_authorTodos.Data?.Count ?? 0} todos");
// Gated on both: step 3 needs the author id that step 2a resolved.
if (_author.Data is { } user && _authorTodos.Data is { Count: > 0 })
{
await _posts.Execute(
DocsKeys.Posts(user.Id, 1),
t => Api.GetPostsAsync(user.Id, 1, t),
o => o.OnError = Fail("3"));
Log($"3 · loaded {_posts.Data?.Count ?? 0} posts for {user.Name}");
}
};
p.OnError = Fail("1");
});
}
private Action<Exception> Fail(string step) => e => _error = $"Step {step} failed: {e.Message}";
private void Log(string message) => _stepLog.Add(message);
}Select — per-component projection#
Select transforms the cached value before it is published to Data.
It is per query, so two components can project one entry differently — the cache always stores
the raw value.
PlaceholderData#
Rendered as Data while the first fetch runs and the cache is empty; never written
to the cache. Use it for skeleton rows that are typed like real data.
A same-key Execute made while one is in flight joins that execution:
the second call's parameters are dropped and no supersede happens. Don't reuse one key
across calls whose parameters differ — omit the key to get supersede semantics instead.
Live demo#
Loading…
@placeholder · shown while the first fetch runs
With no user selected, Enabled = false: the query serves cached data but never
fetches. PlaceholderData renders while the first fetch runs, and
Select projects the cached value per component — the cache keeps the raw user.
@inherits DejaComponentBase
@inject JsonPlaceholderApi Api
<div class="demo-toolbar">
<label>
User:
<select class="demo-input" @onchange="OnUserChanged">
<option value="0" selected="@(_userId == 0)">— none selected —</option>
<option value="1" selected="@(_userId == 1)">User 1</option>
<option value="2" selected="@(_userId == 2)">User 2</option>
<option value="3" selected="@(_userId == 3)">User 3</option>
</select>
</label>
<label>
<input type="checkbox" checked="@_uppercase" @onchange="OnUppercaseChanged"/>
Select: uppercase the name
</label>
</div>
<StateInspector Query="_user" Label="User query"/>
@if (_user.Data is { } user)
{
<div class="demo-panel">
<h4>@user.Name</h4>
<p class="demo-note">@@@user.Username · @user.Email</p>
</div>
}
<p class="demo-note">
With no user selected, <code>Enabled = false</code>: the query serves cached data but never
fetches. <code>PlaceholderData</code> renders while the first fetch runs, and
<code>Select</code> projects the cached value per component — the cache keeps the raw user.
</p>
@code {
private readonly Query<UserDto> _user = new();
private int _userId;
private bool _uppercase;
protected override Task OnInitializedAsync() => Load();
private Task Load() => _user.Execute(DocsKeys.User(_userId), token => Api.GetUserAsync(_userId, token), p =>
{
p.Enabled = _userId > 0;
p.PlaceholderData = new UserDto(0, "Loading…", "placeholder", "shown while the first fetch runs");
if (_uppercase)
{
p.Select = user => user with { Name = user.Name.ToUpperInvariant() };
}
p.OnError = _ => { };
});
private Task OnUserChanged(ChangeEventArgs e)
{
_userId = int.Parse((string)e.Value!);
return Load();
}
private Task OnUppercaseChanged(ChangeEventArgs e)
{
_uppercase = (bool)e.Value!;
return Load();
}
}