Blazor Components
DotNet Query ships two Razor components that make it easy to render query state declaratively in Blazor: <Suspense> and <Transition>. Both components handle subscriptions and re-rendering automatically — you just describe what to show in each state.
Installation
Make sure the Blazor package is installed:
dotnet add package DotNetQuery.Blazor
Then add the namespace to your _Imports.razor:
@using DotNetQuery.Blazor
Suspense
<Suspense> is the straightforward component. It shows a loading indicator while fetching, the data when successful, and an error template on failure. While a background refetch is in progress, it shows the loading template — the old data is hidden.
<Suspense Query="_userQuery">
<Content Context="user">
<h1>@user.Name</h1>
<p>@user.Email</p>
</Content>
<Loading>
<p>Loading user...</p>
</Loading>
<Failure Context="error">
<p class="error">Could not load user: @error.Message</p>
</Failure>
</Suspense>
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
Query |
IQuery<TArgs, TData> |
Yes | The query to render. |
Content |
RenderFragment<TData> |
Yes | Rendered when the query succeeds. |
Loading |
RenderFragment |
No | Rendered while Idle or Fetching. Defaults to nothing. |
Failure |
RenderFragment<Exception> |
No | Rendered on Failure. Defaults to nothing. |
When to Use Suspense
Use <Suspense> when:
- you want a clean loading state between navigations (no stale data flash),
- the data is critical and you do not want to show outdated content,
- you prefer explicit "loading…" states over stale-while-revalidate.
Transition
<Transition> applies stale-while-revalidate semantics: while a background fetch is in progress it keeps showing the last successful data instead of switching to a loading indicator. Only when there is no previous data at all does it fall back to the loading template.
<Transition Query="_productListQuery">
<Content Context="products">
@foreach (var product in products)
{
<ProductCard Product="product" />
}
</Content>
<Loading>
<p>Loading products...</p>
</Loading>
<Failure Context="error">
<p class="error">@error.Message</p>
</Failure>
</Transition>
Parameters
Same as <Suspense>:
| Parameter | Type | Required | Description |
|---|---|---|---|
Query |
IQuery<TArgs, TData> |
Yes | The query to render. |
Content |
RenderFragment<TData> |
Yes | Rendered when data is available (current or last). |
Loading |
RenderFragment |
No | Rendered only when there is no data at all. |
Failure |
RenderFragment<Exception> |
No | Rendered on Failure when no previous data exists. |
When to Use Transition
Use <Transition> when:
- the data updates frequently and you want smooth background refreshes,
- switching to a loading spinner on every refetch would feel jarring,
- you are showing a list that is periodically re-fetched.
Suspense vs Transition at a Glance
| Scenario | Suspense | Transition |
|---|---|---|
| Initial load (no data yet) | Shows Loading |
Shows Loading |
| Background refetch (has old data) | Shows Loading |
Shows old Content |
| Success | Shows Content |
Shows Content |
| Failure (has old data) | Shows Failure |
Shows old Content |
| Failure (no old data) | Shows Failure |
Shows Failure |
Complete Component Example
Here is a full Blazor component using both queries and mutations with the Blazor components. Queries are defined in an injected service class — the component only handles rendering and pushing args.
public sealed class UserProfileQueries(IQueryClient queryClient, HttpClient http) : IDisposable
{
public readonly IQuery<int, UserDto> UserQuery = queryClient.CreateQuery(
new QueryOptions<int, UserDto>
{
KeyFactory = id => QueryKey.From("users", id),
Fetcher = (id, ct) => http.GetFromJsonAsync<UserDto>($"/api/users/{id}", ct)!,
StaleTime = TimeSpan.FromMinutes(5),
});
public readonly IQuery<int, List<PostDto>> PostsQuery = queryClient.CreateQuery(
new QueryOptions<int, List<PostDto>>
{
KeyFactory = id => QueryKey.From("users", id, "posts"),
Fetcher = (id, ct) => http.GetFromJsonAsync<List<PostDto>>($"/api/users/{id}/posts", ct)!,
});
public void Dispose()
{
UserQuery.Dispose();
PostsQuery.Dispose();
}
}
@page "/users/{Id:int}"
@inject UserProfileQueries Queries
<Transition Query="Queries.UserQuery">
<Content Context="user">
<h1>@user.Name</h1>
<button @onclick="() => Queries.UserQuery.Refetch()">Refresh</button>
<Suspense Query="Queries.PostsQuery">
<Content Context="posts">
<ul>
@foreach (var post in posts)
{
<li>@post.Title</li>
}
</ul>
</Content>
<Loading><p>Loading posts...</p></Loading>
</Suspense>
</Content>
<Loading><p>Loading user...</p></Loading>
<Failure Context="error"><p>Error: @error.Message</p></Failure>
</Transition>
@code {
[Parameter] public int Id { get; set; }
protected override void OnParametersSet()
{
Queries.UserQuery.SetArgs(Id);
Queries.PostsQuery.SetArgs(Id);
}
}
QueryRefreshMonitor
<QueryRefreshMonitor> is a side-effect component that automatically invalidates stale queries when the browser comes back online or the user returns to the tab. Place it in your MainLayout.razor — it renders no markup.
@* MainLayout.razor *@
@inherits LayoutComponentBase
<QueryRefreshMonitor />
<div class="page">
@Body
</div>
Invalidation respects each query's StaleTime — fresh data is never re-fetched. Only stale queries trigger a background refetch.
Requires an interactive render mode.
<QueryRefreshMonitor>works with both Blazor WASM and interactive Blazor Server — the browser events fire on the client and call back into .NET over SignalR. It has no effect under static SSR, whereOnAfterRenderAsyncnever runs.
Multi-layout apps: If your app has more than one layout, add
<QueryRefreshMonitor>to each one, or extract it into a shared parent layout that the others inherit from.
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
RefetchOnFocus |
bool |
true |
Invalidates stale queries when the browser tab regains focus. |
RefetchOnReconnect |
bool |
true |
Invalidates stale queries when network connectivity is restored. |
To disable one trigger, pass false for that parameter:
<QueryRefreshMonitor RefetchOnFocus="false" /> <!-- reconnect only -->
<QueryRefreshMonitor RefetchOnReconnect="false" /> <!-- focus only -->
QueryDevTools
<QueryDevTools> is a live cache inspector panel shipped in a separate package. It shows every query in the cache with its current status, observer count, and data, and lets you invalidate or refetch individual queries directly from the browser.
Install the package:
dotnet add package DotNetQuery.Blazor.DevTools
Add it in MainLayout.razor and use IHostEnvironment to limit it to development:
@* MainLayout.razor *@
@inherits LayoutComponentBase
@using DotNetQuery.Blazor.DevTools
@inject IHostEnvironment Env
@if (Env.IsDevelopment())
{
<QueryDevTools />
}
<div class="page">
@Body
</div>
See the DevTools guide for the full feature reference.
Tips
- Use service classes for queries and mutations. Define queries and mutations in a dedicated service that implements
IDisposable, register it with the DI container, and inject it into components. The service owns and disposes the instances — components stay focused on rendering. - Components only dispose their own subscriptions.
<Suspense>and<Transition>dispose their internal subscriptions automatically. If a component subscribes to a query or mutation state stream directly, dispose that subscription inDispose()— but do not dispose the query or mutation itself if it belongs to an injected service. - Push args in
OnParametersSet. When your component receives route parameters, push them insideOnParametersSetso the query updates when the URL changes. - Nest components freely. A
<Suspense>inside a<Transition>'sContentworks perfectly — each component manages its own subscription independently.