Virtualization Table
Handle large datasets efficiently with virtual scrolling.
Preview with Controlled State
View Full State Object
[]
{}{
"pageIndex": 0,
"pageSize": 100
}{}{
"left": [],
"right": []
}Introduction
Section titled “Introduction”The Virtualization Table uses virtual scrolling to efficiently render large datasets (1000+ rows) by only rendering visible rows. This provides smooth performance even with 10,000+ rows.
Installation
Section titled “Installation”Install the DataTable core and add-ons for this example:
First time using
@niko-table? See the Installation Guide to set up the registry.
For other add-ons or manual copy-paste, see the Installation Guide.
Prerequisites
Section titled “Prerequisites”We are going to build a table to show products with virtualization. Here’s what our data looks like:
type Product = { id: string name: string category: string price: number stock: number status: "in-stock" | "low-stock" | "out-of-stock"}
// Generate large datasetconst largeData: Product[] = Array.from({ length: 10000 }, (_, i) => ({ id: `product-${i + 1}`, name: `Product ${i + 1}`, category: "Electronics", price: Math.floor(Math.random() * 500) + 10, stock: Math.floor(Math.random() * 150), status: "in-stock",}))Basic Table with Virtualization
Section titled “Basic Table with Virtualization”Let’s start by building a virtualized table.
Column Definitions
Section titled “Column Definitions”First, we’ll define our columns.
"use client"
import { DataTableColumnHeader } from "@/components/niko-table/components/data-table-column-header"import { DataTableColumnTitle } from "@/components/niko-table/components/data-table-column-title"import { DataTableColumnSortMenu } from "@/components/niko-table/components/data-table-column-sort"import type { DataTableColumnDef } from "@/components/niko-table/types"
export type Product = { id: string name: string category: string price: number stock: number status: "in-stock" | "low-stock" | "out-of-stock"}
export const columns: DataTableColumnDef<Product>[] = [ { accessorKey: "name", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu /> </DataTableColumnHeader> ), meta: { label: "Name" }, cell: ({ row }) => ( <div className="font-medium">{row.getValue("name")}</div> ), }, { accessorKey: "category", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu /> </DataTableColumnHeader> ), meta: { label: "Category" }, }, { accessorKey: "price", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu /> </DataTableColumnHeader> ), meta: { label: "Price" }, cell: ({ row }) => { const price = row.getValue("price") as number return <div className="font-mono">${price.toFixed(2)}</div> }, }, { accessorKey: "stock", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu /> </DataTableColumnHeader> ), meta: { label: "Stock" }, }, { accessorKey: "status", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu /> </DataTableColumnHeader> ), meta: { label: "Status" }, },]<DataTable /> component
Section titled “<DataTable /> component”Next, we’ll create a virtualized table.
"use client"
import { DataTableRoot } from "@/components/niko-table/core/data-table-root"import { DataTable } from "@/components/niko-table/core/data-table"import { DataTableVirtualizedHeader, DataTableVirtualizedBody, DataTableVirtualizedEmptyBody, DataTableVirtualizedSkeleton,} from "@/components/niko-table/core/data-table-virtualized-structure"import { DataTableToolbarSection } from "@/components/niko-table/components/data-table-toolbar-section"import { DataTablePagination } from "@/components/niko-table/components/data-table-pagination"import { DataTableSearchFilter } from "@/components/niko-table/components/data-table-search-filter"import { DataTableViewMenu } from "@/components/niko-table/components/data-table-view-menu"import { DataTableColumnHeader } from "@/components/niko-table/components/data-table-column-header"import { DataTableColumnTitle } from "@/components/niko-table/components/data-table-column-title"import { DataTableColumnSortMenu } from "@/components/niko-table/components/data-table-column-sort"import type { DataTableColumnDef } from "@/components/niko-table/types"
type Product = { id: string name: string category: string price: number stock: number status: "in-stock" | "low-stock" | "out-of-stock"}
const columns: DataTableColumnDef<Product>[] = [ { accessorKey: "name", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu /> </DataTableColumnHeader> ), meta: { label: "Name" }, cell: ({ row }) => ( <div className="font-medium">{row.getValue("name")}</div> ), }, { accessorKey: "category", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu /> </DataTableColumnHeader> ), meta: { label: "Category" }, }, { accessorKey: "price", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu /> </DataTableColumnHeader> ), meta: { label: "Price" }, cell: ({ row }) => { const price = row.getValue("price") as number return <div className="font-mono">${price.toFixed(2)}</div> }, }, { accessorKey: "stock", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu /> </DataTableColumnHeader> ), meta: { label: "Stock" }, }, { accessorKey: "status", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu /> </DataTableColumnHeader> ), meta: { label: "Status" }, },]
export function VirtualizationTable({ data }: { data: Product[] }) { return ( <DataTableRoot data={data} columns={columns} config={{ enablePagination: true, enableSorting: true, enableFilters: true, }} > <DataTableToolbarSection> <DataTableSearchFilter placeholder="Search products..." /> <DataTableViewMenu /> </DataTableToolbarSection>
<DataTable height={600} className="rounded-lg border"> <DataTableVirtualizedHeader /> <DataTableVirtualizedBody> <DataTableVirtualizedSkeleton rows={5} /> <DataTableVirtualizedEmptyBody /> </DataTableVirtualizedBody> </DataTable>
<DataTablePagination pageSizeOptions={[50, 100, 200, 500]} /> </DataTableRoot> )}Virtualized Components
Section titled “Virtualized Components”DataTableVirtualizedHeader
Section titled “DataTableVirtualizedHeader”Virtualized version of the table header.
<DataTableVirtualizedHeader />DataTableVirtualizedBody
Section titled “DataTableVirtualizedBody”Virtualized version of the table body. Only renders visible rows.
<DataTableVirtualizedBody> <DataTableVirtualizedEmptyBody /></DataTableVirtualizedBody>DataTableVirtualizedEmptyBody
Section titled “DataTableVirtualizedEmptyBody”Empty state component for virtualized tables.
<DataTableVirtualizedEmptyBody />DataTableVirtualizedSkeleton
Section titled “DataTableVirtualizedSkeleton”Skeleton loading state for virtualized tables. Shows skeleton rows during data fetching.
<DataTableVirtualizedBody> <DataTableVirtualizedSkeleton rows={5} /> <DataTableVirtualizedEmptyBody /></DataTableVirtualizedBody>DataTableVirtualizedLoading
Section titled “DataTableVirtualizedLoading”Simple loading spinner for virtualized tables. Shows a centered loading indicator.
<DataTableVirtualizedBody> <DataTableVirtualizedLoading /> <DataTableVirtualizedEmptyBody /></DataTableVirtualizedBody>Note: Use DataTableVirtualizedSkeleton for a more detailed loading state that mimics the table structure, or DataTableVirtualizedLoading for a simple spinner.
Required: Fixed Height
Section titled “Required: Fixed Height”Virtualization requires a fixed height on the DataTable component:
<DataTable height={600} className="rounded-lg border"> <DataTableVirtualizedHeader /> <DataTableVirtualizedBody> <DataTableVirtualizedSkeleton rows={5} /> <DataTableVirtualizedEmptyBody /> </DataTableVirtualizedBody></DataTable>Controlled State
Section titled “Controlled State”Manage table state externally for full control:
import { useState } from "react"import type { SortingState, ColumnFiltersState, PaginationState,} from "@tanstack/react-table"
export function ControlledVirtualizationTable({ data }: { data: Product[] }) { const [sorting, setSorting] = useState<SortingState>([]) const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]) const [pagination, setPagination] = useState<PaginationState>({ pageIndex: 0, pageSize: 50, }) const [globalFilter, setGlobalFilter] = useState("")
return ( <DataTableRoot data={data} columns={columns} config={{ enablePagination: true, enableSorting: true, enableFilters: true, }} state={{ sorting, columnFilters, pagination, globalFilter, }} onSortingChange={setSorting} onColumnFiltersChange={setColumnFilters} onPaginationChange={setPagination} onGlobalFilterChange={setGlobalFilter} > <DataTableToolbarSection> <DataTableSearchFilter placeholder="Search products..." /> <DataTableViewMenu /> </DataTableToolbarSection>
<DataTable height={600} className="rounded-lg border"> <DataTableVirtualizedHeader /> <DataTableVirtualizedBody> <DataTableVirtualizedSkeleton rows={5} /> <DataTableVirtualizedEmptyBody /> </DataTableVirtualizedBody> </DataTable>
<DataTablePagination pageSizeOptions={[50, 100, 200, 500]} /> </DataTableRoot> )}When to Use
Section titled “When to Use”✅ Use Virtualization Table when:
- You have 1000+ rows
- Performance is critical
- Users need smooth scrolling
- Dataset is too large for regular pagination
❌ Don’t use Virtualization Table when:
- You have < 1000 rows (use Basic Table)
- You need variable row heights (virtualization works best with fixed heights)
- You need complex row interactions (virtualization has limitations)
Performance Tips
Section titled “Performance Tips”- Fixed height required: Always set a fixed height on
DataTable - Consistent row heights: Virtualization works best with uniform row heights
- Memoize columns: Wrap column definitions in
useMemo - Large page sizes: Use larger page sizes (50-500) for better virtualization
Next Steps
Section titled “Next Steps”- Basic Table - For smaller datasets
- Advanced Table - Combine with other features