The PrimeReact DataTable component is a staple for rendering tabular data in a React app. But if you try to bring in thousands of rows all at once, your application will slow to a crawl. That’s where the lazy loading pattern comes in.
What is Lazy Loading in PrimeReact DataTables?
The lazy loading pattern involves bringing in just enough data, exactly when you need it. For our DataTable, that means streaming in one page at a time — on demand — rather than grabbing the full data set all at once. This is especially important for data coming from a large table, which could span millions (or billions) of rows.
What We’ll Build
In this tutorial, we’ll create a lazy-loaded data table that fetches rows from an ASP.NET Web API backend and displays them in a PrimeReact DataTable inside a Next.js application.
Specifically, you’ll learn how to:
- Set up an ASP.NET Web API endpoint that supports pagination, filtering and sorting.
- Build a Next.js client component that renders a PrimeReact DataTable.
- Enable lazy loading through a Next.js server action.
Requirements
This tutorial uses a local SQL Server database running the AdventureWorks 2022 sample dataset. SQL Server is free to download and install for development workloads. You will also need the .NET SDK and Node.js runtime.
Setting up the Back End in ASP.NET Web API
In Visual Studio (or dotnet CLI), create an ASP.NET Web API project. Install the Dapper and Microsoft.Data.SqlClient Nuget packages.
The first code we’ll write is the model for our data. We’ll be reading from the AW2022.Persons.Persons table, so we’ll create a model with the fields that we would like to display.
namespace Backend
{
public class Person
{
public int BusinessEntityID { get; set; }
public string Title { get; set; }
public string FirstName { get; set; }
public string MiddleName { get; set; }
public string LastName { get; set; }
public string Suffix { get; set; }
}
}
We need some additionally metadata to support pagination — in particular the total record count of the dataset. So, we’ll create a generic class to act as a container for the results and the record count.
namespace Backend
{
public class PaginatedResult<T>
{
public int TotalRecords { get; set; }
public IEnumerable<T> Results { get; set; }
}
}
Finally, we’ll create a Persons controller with a single GET endpoint to serve our data.
using System.Text;
using Dapper;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Data.SqlClient;
namespace Backend.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class PersonsController : ControllerBase
{
[HttpGet]
public async Task<ActionResult<PaginatedResult<Person>>> GetPersons(
[FromQuery] int perPage = 10, [FromQuery] int pageNumber = 0,
[FromQuery] string? sortField = "id", [FromQuery] int sortOrder = 0)
{
var connection = new SqlConnection(
"Data Source=localhost;Database=AW2022;Integrated Security=sspi;TrustServerCertificate=true;");
var from = new StringBuilder("FROM [Person].[Person]");
var sort = new StringBuilder();
if (sortField is not null)
{
sort.Append("ORDER BY ");
switch (sortField)
{
case "businessEntityID": sort.Append("BusinessEntityID"); break;
case "title": sort.Append("Title"); break;
case "firstName": sort.Append("FirstName"); break;
case "middleName": sort.Append("MiddleName"); break;
case "lastName": sort.Append("LastName"); break;
case "suffix": sort.Append("Suffix"); break;
}
if (sortOrder == -1)
{
sort.Append(" DESC");
sort.AppendLine();
}
}
var recordCount = await connection.ExecuteScalarAsync<int>($"SELECT COUNT(*) {from}");
var sql = $"SELECT BusinessEntityID, Title, FirstName, MiddleName, LastName, Suffix {from} {sort} OFFSET {(pageNumber) * perPage} ROWS FETCH NEXT {perPage} ROWS ONLY";
var results = await connection.QueryAsync<Person>(sql);
return new PaginatedResult<Person>()
{
TotalRecords = recordCount,
Results = results
};
}
}
}
The endpoint constructs a dynamic SQL query based on the supplied pagination and sort parameters.
Building out the Front End in Next.js
We’ll bootstrap our Next.js front end with npx create-next-app@latest
. Once your project is created, install the primereact dependency with npm. Replace the main page with a reference to the PersonsDataTable component, which we’ll implement shortly.
import PersonsDataTable from "./persons-data-table";
export default function Home() {
return (
<div className="flex items-center justify-center h-[100vh]">
<div className="w-full max-w-6xl">
<PersonsDataTable />
</div>
</div>
);
}
Finally, we’re ready to implement the data table. In the app directory, create persons-data-table.tsx with the following contents:
'use client'
import { DataTable } from "primereact/datatable";
import { useEffect, useState } from "react";
import getPersons from "./action";
import { Column } from "primereact/column";
import { SortOrder } from "primereact/api";
export default function PersonsDataTable() {
const perPage = 8;
const [loading, setLoading] = useState(false);
const [persons, setPersons] = useState([] as any[]);
const [totalRecords, setTotalRecords] = useState(0);
const [pageNumber, setPageNumber] = useState(0);
const [sortOrder, setSortOrder] = useState(SortOrder.ASC as SortOrder | null | undefined);
const [sortField, setSortField] = useState('businessEntityID');
useEffect(() => {
setLoading(true);
getPersons(pageNumber, perPage, sortField, sortOrder || SortOrder.ASC).then(res => {
setTotalRecords(res.totalRecords);
setPersons(res.results);
setLoading(false);
})
}, [pageNumber, sortOrder, sortField]);
return (
<DataTable
lazy
paginator
value={persons}
loading={loading}
rows={perPage}
onPage={(event) => setPageNumber(event.page!)}
first={pageNumber*perPage}
totalRecords={totalRecords}
onSort={e => {
setSortOrder(e.sortOrder);
setSortField(e.sortField);
}}
sortOrder={sortOrder}
sortField={sortField}
>
<Column field="businessEntityID" header="Id" sortable />
<Column field="title" header="Title" sortable />
<Column field="firstName" header="First Name" sortable />
<Column field="middleName" header="Middle Name" sortable />
<Column field="lastName" header="Last Name" sortable />
<Column field="suffix" header="Suffix" sortable />
</DataTable>
)
}
Note the use of the useState hook to track the current data, as well as the active parameters for pagination and sort. The useEffect hook will refresh our data if any of the parameters change. It depends on the server action (action.ts) implemented below.
'use server'
import { SortOrder } from "primereact/api";
export default async function getPersons(pageNumber: number, perPage: number, sortField: string, sortOrder: SortOrder = SortOrder.ASC) {
return await fetch(`https://localhost:7246/api/Persons?pageNumber=${pageNumber}&perPage=${perPage}&sortField=${sortField}&sortOrder=${sortOrder}`).then(res => res.json());
}
Conclusion
Lazy loading transforms the PrimeReact DataTable from a simple UI component into a high-performance solution capable of handling enterprise-scale datasets. By pairing Next.js on the frontend with ASP.NET Web API on the backend, you can deliver tables that load quickly, respond smoothly to user interactions, and scale as your data grows.
This approach not only improves performance but also provides a better user experience, reduces server strain, and keeps your applications future-ready.
If you’re working with large datasets in a .NET environment, lazy loading is one of the most effective strategies you can implement — and it’s easier than ever with Next.js and PrimeReact.