Summarize with AI:
Learn two ways to properly handle the four loading states: loading, error, success and empty in Blazor web applications.
In modern web development, page rendering speed is crucial. We render pages as quickly as possible and asynchronously load data from the server. Once the data arrives on the client, we update the web application and integrate the data into the existing page.
Therefore, modern data-driven applications are rarely in a stable state. Data might still be loading from the server, the request might fail or the server might return an empty data set.
Handling different application states is essential for creating responsive and user-friendly web applications.
In this article, we will learn how to professionally handle success, loading, empty and error states in Blazor web applications.
You can access the code used in this example on GitHub.
When creating a Blazor Web App based on the .NET 10 project template, you’ll see code like this in the generated Weather page.
@foreach (var forecast in forecasts)
{
<tr>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.TemperatureF</td>
<td>@forecast.Summary</td>
</tr>
}
There is a forecasts field in the @code section of the component of type WeatherForecast[]?.
In the OnInitializedAsync lifecycle method, the forecasts field gets populated with the Weather data.
But what about the initial page render when the forecasts field is null?
If we use the foreach iteration statement on a null value, we’ll get a NullReferenceException during runtime. To prevent that from happening, the default project template wraps the above code in an if-else statement:
@if (forecasts == null)
{
<p><em>Loading...</em></p>
}
else
{
// additional code omitted for simplicity
@foreach (var forecast in forecasts)
{
<tr>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.TemperatureF</td>
<td>@forecast.Summary</td>
</tr>
}
}
As long as the value of the forecasts field is null, the Loading… text is shown on the page. When the forecasts field is populated during page initialization, the page rerenders and the else block is executed, which contains a foreachstatement that iterates over each element of the forecasts array.
While this basic setup works, it has some flaws.
if-else block.null value from the page initialization and the null value from a failed data loading attempt.The goal of this article is to show you how to do a better job handling the empty, loading and error states of Blazor components.
Sometimes the HTTP requests to the server succeeds but returns no items.
In the previous example, we would show an empty list, since the foreach statement will not be executed because the returned list does not contain any items.
A better solution is to explicitly communicate the empty result set to the user. The following code properly differentiates the loading from the empty result set state.
@if (forecasts == null)
{
<p><em>Loading...</em></p>
}
else if (!forecasts.Any())
{
<p>No weather information available.</p>
}
else
{
// additional code omitted for simplicity
@foreach (var forecast in forecasts)
{
<tr>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.TemperatureF</td>
<td>@forecast.Summary</td>
</tr>
}
}
This solution clearly communicates the three different UI states: Loading, empty result set and weather information available.
Another often-overlooked case is when an HTTP request to the server fails.
With the previous implementation, the forecasts field will remain null or set to an empty list (depending on the implementation). Both options are wrong.
It’s best practice to show an error to the user to let them know that something didn’t work properly. Otherwise, they might think that the request returned an empty result set (meaning, no weather data is available).
Consider the following data loading implementation in the OnInitializedAsync lifecycle method:
private List<WeatherForecast>? forecasts;
private string? errorMessage;
protected override async Task OnInitializedAsync()
{
try
{
forecasts = await weatherService.GetWeatherDataAsync();
}
catch (Exception ex)
{
errorMessage = ex.Message;
}
}
We use a try-catch statement to catch exceptions and store the exception message in the errorMessage field.
Hint: It’s best practice to catch and properly handle different exception types. If we cannot recover from a specific exception type, it doesn’t make sense to catch it.
In the component template, we can now show the error message to the user:
@if (errorMessage != null)
{
<p class="text-danger">Error: @errorMessage</p>
}
else if (forecasts == null)
{
<p><em>Loading...</em></p>
}
else if (!forecasts.Any())
{
<p>No weather information available.</p>
}
else
{
// additional code omitted for simplicity
@foreach (var forecast in forecasts)
{
<tr>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.TemperatureF</td>
<td>@forecast.Summary</td>
</tr>
}
}
We first handle the error case before considering the error, loading, empty and success states, using an extended if-else statement with four cases.
Let’s look at a more scalable and less cumbersome approach to handling different loading states using an explicit state enum.
First, we implement a LoadingStates enum with four states: Loading, Success, Empty and Error:
public enum LoadingStates
{
Loading,
Success,
Empty,
Error
}
Next, we add a LoadingState component:
@if (State == LoadingStates.Loading)
{
@Loading
}
else if (State == LoadingStates.Success)
{
@Success
}
else if (State == LoadingStates.Empty)
{
@Empty
}
else if (State == LoadingStates.Error)
{
@Error
}
@code {
[Parameter, EditorRequired]
public LoadingStates State { get; set; }
[Parameter]
public RenderFragment? Loading { get; set; }
[Parameter]
public RenderFragment? Success { get; set; }
[Parameter]
public RenderFragment? Empty { get; set; }
[Parameter]
public RenderFragment? Error { get; set; }
}
We have a required State parameter of type LoadingStates, which will receive the current state of the parent component.
The Loading, Success, Empty and Error properties provide an optional parameter of type RenderFragment. Those properties are then used depending on the state of the parent component.
Hint: You can learn more about templating Blazor components with RenderFragements.
Next, we use the LoadingState component inside the Weather component:
<LoadingState State="@_loadingState">
<Loading>
<p><em>Loading...</em></p>
</Loading>
<Empty>
<p>No weather information available.</p>
</Empty>
<Success>
<table class="table">
<thead>
<tr>
<th>Date</th>
<th aria-label="Temperature in Celsius">Temp. (C)</th>
<th aria-label="Temperature in Fahrenheit">Temp. (F)</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
@foreach (var forecast in forecasts)
{
<tr>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.TemperatureF</td>
<td>@forecast.Summary</td>
</tr>
}
</tbody>
</table>
</Success>
<Error>
<p class="text-danger">Error: @errorMessage</p>
</Error>
</LoadingState>
Instead of the if-else statement, we use the LoadingState component and provide the loading state of the Weather component as its State parameter.
We can now provide HTML definitions for each of the four loading states. For example, we move the table rendering the weather information into the Success child element.
The code section looks like this:
@code {
private IEnumerable<WeatherForecast> forecasts = [];
private LoadingStates _loadingState = LoadingStates.Loading;
private string? errorMessage;
protected override async Task OnInitializedAsync()
{
_loadingState = LoadingStates.Loading;
try
{
forecasts = await WeatherService.GetWeatherDataAsync();
_loadingState = forecasts.Any() ?
LoadingStates.Success :
LoadingStates.Empty;
}
catch (Exception ex)
{
errorMessage = ex.Message;
_loadingState = LoadingStates.Error;
}
}
}
In addition to the forecasts field, we have a _loadingState field and an errorMessage field.
In the OnInitializedAsync method, we first set the loading state to Loading. This allows the Blazor application to communicate that it’s loading data by rendering the Loading state fragment defined in the component template.
Next, we use a try-catch statement to handle unexpected exceptions. In the try clause, we use the WeatherService to fetch the data from the server and set the _loadingState field to Success or Empty depending on the data received.
In case of an exception, we set the error message to the errorMessage field in the catch block and set the _loadingState field to Error.
Hint: You can test the error state by voluntarily throwing an exception inside the
WeatherServiceimplementation.
Optional: You could implement default behavior inside the LoadingState component for the loading and error cases.
That way, you maintain consistency throughout the application and can override the default implementation in specific cases. And you shorten the component template for most components, such as the Weather component in this example.
@if (State == LoadingStateEnum.Loading)
{
@Loading ?? <p>Loading...</p>
}
In the LoadingState component, we use the ?? operator to render the provided render fragment or the default template.
You could do the same for the Error state or maybe even the Empty state.
In data-driven applications, handling states such as loading, error, empty and success is crucial for providing a modern user experience.
The default project template doesn’t provide a solution to handle all of them. It focuses on the happy path while neglecting other cases.
In this article, we learned two ways to properly handle the four loading states. The second approach, using a dedicated LoadingState component, is more reusable and clearly differentiates between all the states.
If you want to learn more about Blazor development, watch my free Blazor Crash Course on YouTube. And stay tuned to the Telerik blog for more Blazor Basics.
Claudio Bernasconi is a passionate software engineer and content creator writing articles and running a .NET developer YouTube channel. He has more than 10 years of experience as a .NET developer and loves sharing his knowledge about Blazor and other .NET topics with the community.