#
Infinite Scroll
#
Implementing Infinite Scroll with htmx and ASP.NET Core
The Infinite Scroll pattern provides a seamless browsing experience where new content is automatically loaded as the user reaches the bottom of the list. This is a popular alternative to traditional pagination, often seen in social media feeds and product listings.
#
The Frontend: Razor & htmx
In Index.cshtml, we start with a standard table. The initial set of rows is rendered via a partial view.
Index.cshtml
<table class="table table-bordered">
<thead>
<tr>
<th>Name</th><th>Email</th><th>ID</th>
</tr>
</thead>
<tbody>
<partial name="_PageResult" model="@Model.Contacts"/>
</tbody>
</table>
The key logic resides in the _PageResult.cshtml partial. It iterates through the contacts and specifically marks the last row of the current set with htmx attributes to trigger the next load.
_PageResult.cshtml
@model List<Contact>
@{
int currentPage = (int)ViewData["PageNumber"];
}
@if (Model.Count > 0)
{
int totalCount = Model.Count;
for (int count = 0; count < totalCount; count++)
{
if ((count + 1) == totalCount)
{
@* The last row triggers the next page load when it is revealed *@
<tr hx-get="/InfiniteScroll/Index?handler=NextPage&Page=@(currentPage + 1)"
hx-trigger="revealed"
hx-swap="afterend">
<td>@Model[count].Name</td>
<td>@Model[count].Email</td>
<td>@Model[count].UniqueIdentifier</td>
</tr>
}
else
{
<tr>
<td>@Model[count].Name</td>
<td>@Model[count].Email</td>
<td>@Model[count].UniqueIdentifier</td>
</tr>
}
}
}
Key htmx attributes used:
hx-get: Requests the next page of data from the server.hx-trigger="revealed": This is the "magic" attribute. It tells htmx to fire the request as soon as the element becomes visible in the viewport (i.e., when the user scrolls to it).hx-swap="afterend": Instead of replacing the target, this appends the returned HTML after the current element, effectively extending the table rows.
#
The Backend: C# PageModel
The server-side code handles the request for the next page. It tracks the current page number and returns a partial view containing the next batch of contacts.
Index.cshtml.cs
public class IndexModel : PageModel
{
[ViewData] public int PageCount { get; set; } = 25;
[ViewData] public int PageNumber { get; set; } = 0;
[FromQuery(Name = "page")] public int NextPage { get; set; }
public List<Contact>? Contacts { get; set; }
public void OnGet()
{
this.Contacts = GetPagedResults(PageNumber, PageCount).ToList();
}
public PartialViewResult OnGetNextPage()
{
PageNumber = NextPage;
var results = GetPagedResults(NextPage, PageCount).ToList();
return Partial("_PageResult", results);
}
private IEnumerable<Contact> GetPagedResults(int page, int take)
{
var start = 10 + (page * take);
for (int i = start; i < start + take; i++)
{
yield return new Contact("User Name", $"user{i}@example.com", Guid.NewGuid());
}
}
}
#
Why this works well
- Superior UX: Users can continue reading without having to stop and click a "Next" button.
- Efficient Loading: Data is only fetched when the user actually scrolls to it, saving bandwidth and server resources for users who only view the top of the list.
- Simple Implementation: Unlike complex JavaScript solutions that require monitoring scroll events and calculating offsets, htmx handles the intersection observation automatically via the
revealedtrigger. - Natural Extension: Using
hx-swap="afterend"on the last row naturally appends the next set of rows to the existing table structure.