I am trying to improve the user experience of my app by showing loading indicators while it retrieves data from a server, but there seems to be something wrong with my understanding of how the lifecycle events fire.
On app launch, I direct users to a Landing page component with a loading spinner whilst it preloads some stuff from a server, before redirecting users to a Dashboard page component.
What I'd like it to do is open the Dashboard page as instantly as possible, with the default content being a loading spinner.
The content side of things I have no problem with.
`OnInitializedAsync' does nothing much. It is at this point that I would expect the user interface to be rendered to the user. But it doesn't.
protected override async Task OnInitializedAsync()
{
Quote = CommonFunctions.RandomQuote();
_pageHistoryService.AddPageToHistory("/dashboard");
}
I also have an OnAfterRenderAsync
method. This method I would expect to run in the background after the page has loaded.
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
GetData();
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(15));
while (!_timerCancellationToken.IsCancellationRequested && await timer.WaitForNextTickAsync())
{
GetData();
}
return;
}
}
In reality, the UI doesn't display the DOM until after the OnAfterRenderAsync
method is completed. Which doesn't make any sense to me.
Is there something I am misunderstanding, or some setting or parameter I need to set in order to get the behaviour I want?
There are several issues in your code. The principle one is you are setting up and running the timer within the component lifecycle: the first call to OnAfterRenderAsync
never completes. You should set up the timer within OnInitialized{Async}
and then let the timer drive updates outside the lifecycle.
Here's a simple demo to show one way to refactor the code you've shown. For simplicity, it just updates the time every second. There's commentary in the code to explain various points.
@page "/"
@implements IDisposable
<h1>Hello, world!</h1>
Welcome to your new app.
<div class="bg-dark text-white m-2 p-2">
@if(_value is null)
{
<pre>Loading....</pre>
}
else
{
<pre>VALUE: @_value</pre>
}
</div>
@code{
private Timer? _timer;
private string? _value;
protected override Task OnInitializedAsync()
{
// Set up time to run every second with the initial run immediately
_timer = new(this.OnTimerElapsed, null, TimeSpan.Zero, TimeSpan.FromSeconds(1));
return Task.CompletedTask;
}
// This is the timer callback method.
// It's run outside the component lifecycle and potentially on another thread
private async void OnTimerElapsed(object? state)
{
// Fake async call to get data
await Task.Delay(100);
_value = DateTime.Now.ToLongTimeString();
// invoke on the UI Thread/Sync Context
await this.InvokeAsync(StateHasChanged);
}
protected override void OnAfterRender(bool firstRender)
{
// General rule: Do nothing here except JS stuff
}
// Need to implement IDisposable and dispose the timer properly
public void Dispose()
{
_timer?.Dispose();
}
}
Also review these answers [and many more] which explain more about OnAfterRender.