Infinite scrolling in Blazor
Infinite scrolling is a way to automatically loads data when you reach the end of the page. It allows you to continue scrolling indefinitely. The method is often used in social media feeds or blogs.
In this post, we'll create a Blazor component that you can use like the following:
<InfiniteScrolling ItemsProvider="GetItems">
<ItemTemplate Context="item">
<p>Item @item</p>
</ItemTemplate>
<LoadingTemplate>
<div>Loading...</div>
</LoadingTemplate>
</InfiniteScrolling>
@code {
async Task<IEnumerable<int>> GetItems(InfiniteScrollingItemsProviderRequest request)
{
await Task.Delay(1000); // Simulate async loading
return Enumerable.Range(request.StartIndex, 50);
}
}
Demo of the infinite scrolling Blazor component
#Infinite scrolling implementation
The idea is to have an invisible element after the display element. When this element enters into the viewport, it means you need to load new items. You can be notified when the element enters into the viewport using the IntersectionObserver
API.
First, you need to create a file named infinite-scrolling.js
under wwwroot
with the following content:
export function initialize(lastItemIndicator, componentInstance) {
const options = {
root: findClosestScrollContainer(lastItemIndicator),
rootMargin: '0px',
threshold: 0,
};
const observer = new IntersectionObserver(async (entries) => {
// When the lastItemIndicator element is visible => invoke the C# method `LoadMoreItems`
for (const entry of entries) {
if (entry.isIntersecting) {
observer.unobserve(lastIndicator);
await componentInstance.invokeMethodAsync("LoadMoreItems");
}
}
}, options);
observer.observe(lastItemIndicator);
// Allow to cleanup resources when the Razor component is removed from the page
return {
dispose: () => dispose(observer),
onNewItems: () => {
observer.unobserve(lastIndicator);
observer.observe(lastIndicator);
},
};
}
// Cleanup resources
function dispose(observer) {
observer.disconnect();
}
// Find the parent element with a vertical scrollbar
// This container should be use as the root for the IntersectionObserver
function findClosestScrollContainer(element) {
while (element) {
const style = getComputedStyle(element);
if (style.overflowY !== 'visible') {
return element;
}
element = element.parentElement;
}
return null;
}
Then, you can create a small C# file with the following content:
public sealed class InfiniteScrollingItemsProviderRequest
{
public InfiniteScrollingItemsProviderRequest(int startIndex, CancellationToken cancellationToken)
{
StartIndex = startIndex;
CancellationToken = cancellationToken;
}
public int StartIndex { get; }
public CancellationToken CancellationToken { get; }
}
public delegate Task<IEnumerable<int>> ItemsProviderRequestDelegate(InfiniteScrollingItemsProviderRequest request);
Finally, you can create the razor component InfiniteScrolling.razor
:
@using System.Threading
@typeparam T
@inject IJSRuntime JSRuntime
@implements IAsyncDisposable
@foreach (var item in _items)
{
@ItemTemplate(item)
}
@if (_loading)
{
@LoadingTemplate
}
@if (!_enumerationCompleted)
{
<div @ref="_lastItemIndicator" style="height:1px;flex-shrink:0"></div>
}
@code {
private List<T> _items = new();
private ElementReference _lastItemIndicator;
private DotNetObjectReference<InfiniteScrolling<T>> _currentComponentReference;
private IJSObjectReference _module;
private IJSObjectReference _instance;
private bool _loading = false;
private bool _enumerationCompleted = false;
private CancellationTokenSource _loadItemsCts;
[Parameter]
public ItemsProviderRequestDelegate<T> ItemsProvider { get; set; }
[Parameter]
public RenderFragment<T> ItemTemplate { get; set; }
[Parameter]
public RenderFragment LoadingTemplate { get; set; }
[JSInvokable]
public async Task LoadMoreItems()
{
if (_loading)
return;
_loading = true;
try
{
_loadItemsCts ??= new CancellationTokenSource();
StateHasChanged(); // Allow the UI to display the loading indicator
try
{
var newItems = await ItemsProvider(new InfiniteScrollingItemsProviderRequest(_items.Count, _loadItemsCts.Token));
var previousCount = items.Count;
items.AddRange(newItems);
if (items.Count == previousCount)
{
_enumerationCompleted = true;
}
else
{
await _instance.InvokeVoidAsync("onNewItems");
}
}
catch (OperationCanceledException oce) when (oce.CancellationToken == _loadItemsCts.Token)
{
// No-op; we canceled the operation, so it's fine to suppress this exception.
}
}
finally
{
_loading = false;
}
StateHasChanged(); // Display the new items and hide the loading indicator
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
// Initialize the IntersectionObserver
if (firstRender)
{
_module = await JSRuntime.InvokeAsync<IJSObjectReference>("import", "./infinite-scrolling.js");
_currentComponentReference = DotNetObjectReference.Create(this);
_instance = await _module.InvokeAsync<IJSObjectReference>("initialize", _lastItemIndicator, _currentComponentReference);
}
}
public async ValueTask DisposeAsync()
{
// Cancel the current load items operation
if (_loadItemsCts != null)
{
_loadItemsCts.Dispose();
_loadItemsCts = null;
}
// Stop the IntersectionObserver
if (_instance != null)
{
await _instance.InvokeVoidAsync("dispose");
await _instance.DisposeAsync();
_instance = null;
}
if (_module != null)
{
await _module.DisposeAsync();
}
_currentComponentReference?.Dispose();
}
}
#Additional resources
Do you have a question or a suggestion about this post? Contact me!