Using IAsyncEnumerable in a Razor component

 
 
  • Gérald Barré

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!

Follow me:
Enjoy this blog?