Beware of using await periodicTimer.WaitForNextTickAsync(). This method is appealing due to its asynchronous signature, making it easy to set up periodic tasks, which might tempt you to implement UI updates with it, especially in Blazor:
protected override async Task OnInitializedAsync()
{
await StartTimerAsync();
}
private async Task StartTimerAsync()
{
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(10));
while (await timer.WaitForNextTickAsync())
{
// do some UI updates here
}
}
Warning! While this approach doesn’t block the UI thread thanks to async-await, the issue is that the method calling such code never actually completes.
For instance, if StartTimerAsync() is called directly from OnInitializedAsync, OnParametersSetAsync, OnAfterRenderAsync, or an action callback, the parent method will never finish, leading to some unexpected consequences, like:
- If called from an
HxButton.OnClickbutton handler, the spinner remains stuck, never stopping.- The button also stays under single-click protection, disabled, and unusable.
- If called from
OnInitializedAsync, the first roundtrip won’t invokeOnParametersSet[Async], which won’t execute until a new roundtrip arrives. - Calling from
OnParametersSetAsyncleaves an unfinished task inComponentBase.CallStateHasChangedOnAsyncCompletion()and requires handling to prevent multiple timers from starting, asOnParametersSetAsyncis called repeatedly. - If called from
OnAfterRenderAsync(bool firstRender), it could block theawait base.OnAfterRenderAsync(firstRender)call, disrupting inherited functionality (especially crucial forfirstRender = true, which only runs once).
So, PeriodicTimer.WaitForNextTickAsync() is more appropriate in scenarios where it’s safe for the calling code to continue indefinitely, such as in a Main method for a console application handling cyclical tasks or within BackgroundService.ExecuteAsync(). In general, however, the calling method should be allowed to complete. Instead, a traditional setup using Task.Run(..) is recommended, placing the timer (or even a regular Timer) on the ThreadPool without awaiting its completion in the current method (fire-and-forget). In Blazor, this requires manually invoking StateHasChanged() or possibly DispatchExceptionAsync().
Example:
public MyComponent : IDisposable
{
private PeriodicTimer timer;
protected override async Task OnInitializedAsync()
{
_ = Task.Run(StartTimerAsync);
}
private async Task StartTimerAsync()
{
timer = new PeriodicTimer(TimeSpan.FromSeconds(10));
while (await timer.WaitForNextTickAsync())
{
// do some UI updates here
StateHasChanged(); // as needed
}
}
public void Dispose()
{
timer?.Dispose();
}
}
Don’t forget cleanup with timer.Dispose(), or the Timer will keep running even after the component is destroyed, leading to resource leaks.
See also the ASP.NET Core Blazor documentation:
ASP.NET Core Blazor synchronization context: Invoke component methods externally to update state