Blazor makes it straightforward to load data in your components, but there are a few key things to watch out for.
Blazor makes it pretty straightforward to retrieve data and use it to render your components.
In most cases you can put your API (or direct service calls, if using Blazor Server) in the OnIntialized
(or async equivalent) method, store the resulting data in a field and let Blazor render it from there.
@page "/Products"
@inject ITopSellersQuery topSellersQuery
<ul>
@foreach (var product in TopSellingProducts)
{
<li>@product.Name</li>
}
</ul>
@code {
private IList<ProductDetails> TopSellingProducts;
protected override Task OnInitializedAsync()
{
TopSellingProducts = topSellersQuery.List();
return base.OnInitializedAsync();
}
}
But as your app grows, and you start adding more and more components, key questions and challenges start to emerge.
Here are three top tips to keep your Blazor app running smoothly (and easy for you to develop, extend and maintain).
As you add more components to your app, you may start to wonder whether they should fetch their own data.
For example, imagine a component which retrieves a list of products and displays them as “cards” in the UI.
One option would be to fetch the list of products at the “top” level and pass the product details down to a card component.
Alternatively, you could let each product card retrieve its own details.
So which is right? Well, as ever, context matters.
Every time you make a call to fetch data you’re introducing a side effect to a component. As well as the component’s primary role (to handle UI logic, render UI and react to user interactions), it now needs to make a call to somewhere else (to fetch data).
Introduce too many side effects at different “levels” in your application, and you run the risk of it being harder to predict how any given component is going to behave.
Side effects are, by their nature, unpredictable. New data can suddenly arrive which causes your component to behave differently.
In this case, where we’re displaying a number of products in a grid, it makes sense to make one call to fetch the data in the top-level ProductList
component, then loop through each product and render a ProductCard
for
each one.
ProductList.razor
<ul>
@foreach (var product in TopSellingProducts)
{
<li>
<ProductCard Details="product"/>
</li>
}
</ul>
We can keep the ProductCard
component itself nice and simple.
ProductCard.razor
<div>
@Details.Name
</div>
<div>
@Details.Description
</div>
@code {
[Parameter]
public ProductDetails Details { get; set; }
}
With this approach, it’s fairly simple to add things like filtering, sorting and paging as we can re-run the entire query in ProductList
, fetch the updated data, and the Product Cards will take care of themselves.
We can also take care to retrieve just the fields we need (based on what we’re displaying in that ProductCard
component), thereby avoiding over-fetching data from the backend/API.
But what about a product details “page”?
In that case, it almost certainly makes sense to introduce a separate ProductDetails
component which fetches its own data, for a few reasons.
For these reasons, the product details page lends itself to being a “top-level page,” which fetches its own data, rather than a sub-component.
As a general rule, I try to stick to fetching data at the “page” level, passing data down to any components rendered on that same “page.” Anything you navigate to as a separate page would then fetch its own data.
This brings a few advantages:
Naturally, this isn’t a hard and fast rule.
For example, if you have something like a dashboard, showing lots of disparate information, it would be logical to let each dashboard widget fetch its own data.
You wouldn’t want to fetch all the data for every dashboard widget at the top “dashboard page” level because the data for each widget is largely separate, unrelated data, so fetching it all together doesn’t bring many benefits (and directly couples all the widgets to the dashboard itself).
What if you need to load a lot of data and render it in your Blazor app? We’re talking thousands, hundreds of thousands or even millions of rows.
If you try to use a foreach
loop with millions of records, your Blazor UI will attempt to render all of them and come to a juddering halt.
Happily, the Virtualize
component can bring your app back from the brink, and make it snappy once again!
ProductList.razor
<ul>
<Virtualize Items="@TopSellingProducts" Context="product">
<li>
<ProductCard Details="product"/>
</li>
</Virtualize>
</ul>
Blazor will now render just the number of elements that are visible on the screen. As you scroll down the page, it will keep reusing those same elements, swapping out the data as you go.
Your app, running in the browser, remains responsive, despite the sheer number of records you’ve loaded in the background.
With this approach, your component will still fetch and load all the records into memory, but it won’t try to render them all at once.
If you want to go one step further and lazy load this data (only retrieving the data which is actually visible on the screen), you can via the ItemsProvider
parameter.
ProductList.razor
@page "/Products"
@inject ITopSellersQuery topSellersQuery
<ul>
<Virtualize ItemsProvider="LoadProductDetails" Context="product">
<li>
<ProductCard Details="product"/>
</li>
</Virtualize>
</ul>
Now, instead of passing a list of prepopulated items to the component we’ve told it how to fetch its own data.
private async ValueTask<ItemsProviderResult<ProductDetails>> LoadProductDetails(ItemsProviderRequest request)
{
var totalCount = TopSellersQuery.Count();
var numProducts = Math.Min(request.Count, totalCount - request.StartIndex);
var results = topSellersQuery.List (request.StartIndex, numProducts);
return new ItemsProviderResult<ProductDetails>(results, totalCount);
}
LoadProductDetails
does a little work to figure out the index of the first record we want to load.
Request.count
refers to the number of records the Virtualize component needs to render (which will vary, depending on things like screen size and if the browser window is maximized or not).
We need to tell Virtualize
what the total record count is (hence the call to retrieve that count first—we could also do this once and store it in a field).
We do a quick bit of number crunching to make sure we know how many records to retrieve:
Math.Min(request.Count, totalCount - request.StartIndex);
For example, if Virtualize
requested 80 records (because that’s how many it could render), but we only had 50 total records, this would ensure we only attempted to load 50 (as that’s all we’ve got!).
After that, it’s a case of telling our query which record to start with and how many to load.
var results = topSellersQuery.List (request.StartIndex, numProducts);
When we return these results, Virtualize
does the rest and renders the returned data.
Finally, if you plan to support Server and WASM, you may not want to make calls over HTTP in every case. It feels redundant to make an API call when you’re running under Blazor Server and already have access to the database itself.
But for WASM you would need that HTTP call (as WASM runs in the browser, which has no direct access to the data).
The easiest solution here is to employ an interface, then switch out the implementation for the different hosting models.
public interface ITopSellersQuery
{
IEnumerable<ProductDetails> List(int startIndex, int numProducts);
}
You can use the interface in your component:
@page "/Products"
@inject ITopSellersQuery topSellersQuery
Put this component in a shared class library and both Server/WASM clients can render it.
From there you’d want two implementations of ITopSellersQuery
—a WASM version:
class TopSellersQueryWASM : ITopSellersQuery
{
private readonly HttpClient _httpClient;
public TopSellersQueryWASM(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<IEnumerable<ProductDetails>> List(int startIndex, int numProducts)
{
var requestUri =
$"api/products?skip={startIndex}&take={numProducts}";
return await _httpClient
.GetFromJsonAsync<IEnumerable<ProductDetails>>(requestUri);
}
}
And a Server version:
public class class TopSellersQuery : ITopSellersQuery {
public IEnumerable<ProductDetails> List(int startIndex, int numProducts)
{
return Enumerable.Range(startIndex, numProducts)
.Select(x => new ProductDetails
{
Id = x,
Description = "Test Product Description",
Name = "A Product",
Price = 200m
});
}
}
Here we’re using hardcoded data, but this could equally be a call to a database (using EF Core, Dapper or any other ORM you fancy).
If you register the WASM version in your WASM project and the Server version in your Blazor Server project, you should be good to go.
You can skip the extra network cycles when you’re running Blazor Server and access the data direct, but still use the same, shared component for Blazor WASM (this time invoking the same code via HTTP).
To make the WASM version work, you’ll need to expose an endpoint (in the server part of your app) which invokes that same query.
Here’s an example using minimal APIs:
app.MapGet("api/products", async (
HttpContext context,
ITopSellersQuery query,
[FromQuery] int skip,
[FromQuery] int take) => await query.List(skip, take));
With that, you can reuse your components on Blazor Server and WASM, confident that your app will take the most efficient route to load the data.
Blazor has a simple and scalable mechanism for loading and rendering data in a component.
Most of the time it “just works,” but remember these three tips if you need to retrieve lots of data, or just want to be able to come back to your app in a few months and easily figure out where that data’s coming from:
Virtualize
if you need to handle lots of records.Curious about Blazor for .NET MAUI? Read Jon’s thoughts about what, how and when to use it.
Jon spends his days building applications using Microsoft technologies (plus, whisper it quietly, a little bit of JavaScript) and his spare time helping developers level up their skills and knowledge via his blog, courses and books. He's especially passionate about enabling developers to build better web applications by mastering the tools available to them. Follow him on Twitter here.