IAsyncEnumerable<T> is an interface for streaming asynchronous data. A common use case is fetching data from a paginated REST API, where multiple async HTTP requests are needed to retrieve all items. This article demonstrates how to consume a method returning IAsyncEnumerable<T> in a Razor component.
Razor
@* ❌ This is not valid in a Blazor component as the rendering must be synchronous *@
<ul>
@await foreach (var item in GetDataAsync())
{
<li>@item</div>
}
</ul>
#Naive implementation
One solution is to use a List<T> to collect items from the enumerator and trigger a re-render whenever a new item arrives by calling StateHasChanged(). You can start the enumeration in any lifecycle method of the Razor component, such as OnInitializedAsync or OnParametersSetAsync.
Razor
<ul>
@foreach (var item in items)
{
<li>@item</li>
}
</ul>
@code {
List<string> items = new();
protected override async Task OnInitializedAsync()
{
await foreach (var text in GetStringsAsync())
{
items.Add(text);
StateHasChanged();
}
}
private async IAsyncEnumerable<string> GetStringsAsync()
{
for (int i = 0; i < 10; i++)
{
yield return $"item {i}";
await Task.Delay(100);
}
}
}
This implementation works but is inefficient. StateHasChanged() may be called many times within a very short interval. A better approach is to wait a few hundred milliseconds between calls so that a fast enumerator triggers only one re-render instead of many.
#Throttling calls to StateHasChanged
I've already written about throttling events in a previous post. The idea is to call StateHasChanged() only once every few hundred milliseconds.
Razor
@page "/throttle"
<PageTitle>Index</PageTitle>
<ul>
@foreach (var item in items)
{
<li>@item</li>
}
</ul>
@code {
List<string> items = new();
protected override async Task OnInitializedAsync()
{
var throttledStateHasChanged = Throttle(
() => InvokeAsync(StateHasChanged),
TimeSpan.FromMilliseconds(500));
await foreach (var text in GetStringsAsync())
{
items.Add(text);
throttledStateHasChanged();
}
}
private async IAsyncEnumerable<string> GetStringsAsync()
{
for (int i = 0; i < 10; i++)
{
yield return $"item {i}";
await Task.Delay(100);
}
}
// https://www.meziantou.net/debouncing-throttling-javascript-events-in-a-blazor-application.htm#throttle-debounce-on
private static Action Throttle(Action action, TimeSpan interval)
{
Task task = null;
var l = new object();
return () =>
{
if (task != null)
return;
lock (l)
{
if (task != null)
return;
task = Task.Delay(interval).ContinueWith(t =>
{
action();
task = null;
});
}
};
}
}
#Canceling the enumeration when the component is removed from the page
You should also stop the enumeration when the component is removed from the page to avoid unnecessary CPU and I/O work. The await foreach operator supports cancellation via the WithCancellation method. Create a CancellationTokenSource, pass its token to the enumerator, and call Cancel in the Dispose method. Blazor calls the Dispose method when the component is removed from the page.
Razor
@implements IDisposable
<ul>
@foreach (var item in items)
{
<li>@item</li>
}
</ul>
@code {
private CancellationTokenSource cts = new();
List<string> items = new();
public void Dispose()
{
cts.Cancel();
cts.Dispose();
}
protected override async Task OnInitializedAsync()
{
var stateHasChangedThrottled = Throttle(() => InvokeAsync(StateHasChanged), TimeSpan.FromMilliseconds(500));
await foreach (var text in GetStringsAsync())
await foreach (var text in GetStringsAsync().WithCancellation(cts.Token))
{
items.Add(text);
stateHasChangedThrottled();
}
}
private async IAsyncEnumerable<string> GetStringsAsync(CancellationToken cancellationToken = default)
private async IAsyncEnumerable<string> GetStringsAsync([EnumeratorCancellation] CancellationToken cancellationToken = default)
{
for (int i = 0; i < 100; i++)
{
yield return $"item {i}";
await Task.Delay(100, cancellationToken);
}
}
}
Do you have a question or a suggestion about this post? Contact me!