Row & Column Virtualization
LyteNyte Grid virtualizes rows and columns. This guide explains virtualization, highlights its performance benefits, and outlines key considerations for developers.
What Is Virtualization
Virtualization determines which elements are visible in the viewport and renders only those elements. The grid does not render elements outside the viewport. Whenever the viewport changes, such as during scrolling or resizing, the grid recalculates the visible range and updates the rendered elements.
In this guide, virtualization refers to UI rendering and not to hardware or OS-level virtualization. Despite the similarity in naming, these are distinct concepts with no overlap.
LyteNyte Grid virtualizes rows and columns only displaying the cells that are visible within the viewport. LyteNyte Grid's virtualization additionally renders some rows above and below the viewport, and some columns to the left and right of the viewport to improve the perceived performance of scrolling. Virtualization serves as the grid's primary performance optimization, and LyteNyte Grid enables it by default.
To see virtualization in action, inspect the grid below using your browser's developer tools. As you scroll, you will see rows mount and unmount. That behavior shows virtualization at work.
Virtualized Grid
"use client";import "./main.css";import { Grid, useClientRowDataSource } from "@1771technologies/lytenyte-pro";import "@1771technologies/lytenyte-pro/grid.css";import type { Column } from "@1771technologies/lytenyte-pro/types";import { useId } from "react";import {DateCell,Header,LatencyCell,MethodCell,PathnameCell,RegionCell,RowDetailRenderer,StatusCell,TimingPhaseCell,} from "./components";import { type RequestData, requestData } from "./data";import { ChevronDownIcon, ChevronRightIcon } from "@1771technologies/lytenyte-pro/icons";const columns: Column<RequestData>[] = [{id: "Date",name: "Date",width: 200,cellRenderer: DateCell,type: "datetime",},{ id: "Status", name: "Status", width: 100, cellRenderer: StatusCell },{id: "Method",name: "Method",width: 100,cellRenderer: MethodCell,},{ id: "timing-phase", name: "Timing Phase", cellRenderer: TimingPhaseCell },{ id: "Pathname", name: "Pathname", cellRenderer: PathnameCell },{id: "Latency",name: "Latency",width: 120,cellRenderer: LatencyCell,type: "number",},{ id: "region", name: "Region", cellRenderer: RegionCell },];export default function Demo() {const ds = useClientRowDataSource<RequestData>({data: requestData,});const grid = Grid.useLyteNyte({gridId: useId(),columns,rowDetailHeight: 200,rowDetailRenderer: RowDetailRenderer,columnMarkerEnabled: true,columnMarker: {width: 40,cellRenderer: ({ row, grid }) => {const isExpanded = grid.api.rowDetailIsExpanded(row);return (<buttonclassName="text-ln-gray-70 flex h-full w-[calc(100%-1px)] items-center justify-center pl-2"onClick={() => grid.api.rowDetailToggle(row)}>{isExpanded ? (<ChevronDownIcon width={20} height={20} />) : (<ChevronRightIcon width={20} height={20} />)}</button>);},},columnBase: {headerRenderer: Header,},rowDataSource: ds,});const view = grid.view.useValue();return (<div className="lng-grid" style={{ width: "100%", height: "400px" }}><Grid.Root grid={grid}><Grid.Viewport><Grid.Header>{view.header.layout.map((row, i) => {return (<Grid.HeaderRow headerRowIndex={i} key={i}>{row.map((c) => {if (c.kind === "group")return <Grid.HeaderGroupCell cell={c} key={c.idOccurrence} />;return (<Grid.HeaderCell cell={c} key={c.column.id} className="after:bg-ln-gray-20" />);})}</Grid.HeaderRow>);})}</Grid.Header><Grid.RowsContainer><Grid.RowsCenter>{view.rows.center.map((row) => {if (row.kind === "full-width") return <Grid.RowFullWidth row={row} key={row.id} />;return (<Grid.Row key={row.id} row={row} accepted={["row"]}>{row.cells.map((cell) => {return <Grid.Cell cell={cell} key={cell.id} />;})}</Grid.Row>);})}</Grid.RowsCenter></Grid.RowsContainer></Grid.Viewport></Grid.Root></div>);}
import type {CellRendererParams,HeaderCellRendererParams,RowDetailRendererParams,SortComparatorFn,SortModelItem,} from "@1771technologies/lytenyte-pro/types";import type { RequestData } from "./data";import { format } from "date-fns";import { useMemo } from "react";import clsx from "clsx";import { PieChart } from "react-minimal-pie-chart";import { ArrowDownIcon, ArrowUpIcon } from "@1771technologies/lytenyte-pro/icons";const colors = ["var(--transfer)", "var(--dns)", "var(--connection)", "var(--ttfb)", "var(--tls)"];const customComparators: Record<string, SortComparatorFn<RequestData>> = {region: (left, right) => {if (left.kind === "branch" || right.kind === "branch") {if (left.kind === "branch" && right.kind === "branch") return 0;if (left.kind === "branch" && right.kind !== "branch") return -1;if (left.kind !== "branch" && right.kind === "branch") return 1;}if (!left.data || !right.data) return !left.data ? 1 : -1;const leftData = left.data as RequestData;const rightData = right.data as RequestData;return leftData["region.fullname"].localeCompare(rightData["region.fullname"]);},"timing-phase": (left, right) => {if (left.kind === "branch" || right.kind === "branch") {if (left.kind === "branch" && right.kind === "branch") return 0;if (left.kind === "branch" && right.kind !== "branch") return -1;if (left.kind !== "branch" && right.kind === "branch") return 1;}if (!left.data || !right.data) return !left.data ? 1 : -1;const leftData = left.data as RequestData;const rightData = right.data as RequestData;return leftData.Latency - rightData.Latency;},};export function Header({ column, grid }: HeaderCellRendererParams<RequestData>) {const sort = grid.state.sortModel.useValue().find((c) => c.columnId === column.id);const isDescending = sort?.isDescending ?? false;return (<divclassName="text-ln-gray-60 flex h-full w-full items-center px-4 text-sm transition-all"onClick={() => {const current = grid.api.sortForColumn(column.id);if (current == null) {let sort: SortModelItem<RequestData>;const columnId = column.id;if (customComparators[column.id]) {sort = {columnId,sort: {kind: "custom",columnId,comparator: customComparators[column.id],},};} else if (column.type === "datetime") {sort = {columnId,sort: { kind: "date", options: { includeTime: true } },};} else if (column.type === "number") {sort = { columnId, sort: { kind: "number" } };} else {sort = { columnId, sort: { kind: "string" } };}grid.state.sortModel.set([sort]);return;}if (!current.sort.isDescending) {grid.state.sortModel.set([{ ...current.sort, isDescending: true }]);} else {grid.state.sortModel.set([]);}}}>{column.name ?? column.id}{sort && (<>{!isDescending ? (<ArrowUpIcon className="size-4" />) : (<ArrowDownIcon className="size-4" />)}</>)}</div>);}export function DateCell({ column, row, grid }: CellRendererParams<RequestData>) {const field = grid.api.columnField(column, row);const niceDate = useMemo(() => {if (typeof field !== "string") return null;return format(field, "MMM dd, yyyy HH:mm:ss");}, [field]);// Guard against bad values and render nothingif (!niceDate) return null;return <div className="text-ln-gray-100 flex h-full w-full items-center px-4">{niceDate}</div>;}export function StatusCell({ column, row, grid }: CellRendererParams<RequestData>) {const status = grid.api.columnField(column, row);// Guard against bad valuesif (typeof status !== "number") return null;return (<div className={clsx("flex h-full w-full items-center px-4 text-xs font-bold")}><divclassName={clsx("rounded-sm px-1 py-px",status < 400 && "text-ln-primary-50 bg-[#126CFF1F]",status >= 400 && status < 500 && "bg-[#FF991D1C] text-[#EEA760]",status >= 500 && "bg-[#e63d3d2d] text-[#e63d3d]",)}>{status}</div></div>);}export function MethodCell({ column, row, grid }: CellRendererParams<RequestData>) {const method = grid.api.columnField(column, row);// Guard against bad valuesif (typeof method !== "string") return null;return (<div className={clsx("flex h-full w-full items-center px-4 text-xs font-bold")}><divclassName={clsx("rounded-sm px-1 py-px",method === "GET" && "text-ln-primary-50 bg-[#126CFF1F]",(method === "PATCH" || method === "PUT" || method === "POST") &&"bg-[#FF991D1C] text-[#EEA760]",method === "DELETE" && "bg-[#e63d3d2d] text-[#e63d3d]",)}>{method}</div></div>);}export function PathnameCell({ column, row, grid }: CellRendererParams<RequestData>) {const path = grid.api.columnField(column, row);if (typeof path !== "string") return null;return (<div className="text-ln-gray-90 flex h-full w-full items-center px-4 text-sm"><div className="text-ln-primary-50 w-full overflow-hidden text-ellipsis text-nowrap">{path}</div></div>);}const numberFormatter = new Intl.NumberFormat("en-Us", {maximumFractionDigits: 0,minimumFractionDigits: 0,});export function LatencyCell({ column, row, grid }: CellRendererParams<RequestData>) {const ms = grid.api.columnField(column, row);if (typeof ms !== "number") return null;return (<div className="text-ln-gray-90 flex h-full w-full items-center px-4 text-sm tabular-nums"><div><span className="text-ln-gray-100">{numberFormatter.format(ms)}</span><span className="text-ln-gray-60 text-xs">ms</span></div></div>);}export function RegionCell({ grid, row }: CellRendererParams<RequestData>) {// Only render for leaf rows and we have some dataif (!grid.api.rowIsLeaf(row) || !row.data) return null;const shortName = row.data["region.shortname"];const longName = row.data["region.fullname"];return (<div className="flex h-full w-full items-center"><div className="flex items-baseline gap-2 px-4 text-sm"><div className="text-ln-gray-100">{shortName}</div><div className="text-ln-gray-60 leading-4">{longName}</div></div></div>);}export function TimingPhaseCell({ grid, row }: CellRendererParams<RequestData>) {// Guard against rows that are not leafs or rows that have no data.if (!grid.api.rowIsLeaf(row) || !row.data) return;const total =row.data["timing-phase.connection"] +row.data["timing-phase.dns"] +row.data["timing-phase.tls"] +row.data["timing-phase.transfer"] +row.data["timing-phase.ttfb"];const connectionPer = (row.data["timing-phase.connection"] / total) * 100;const dnsPer = (row.data["timing-phase.dns"] / total) * 100;const tlPer = (row.data["timing-phase.tls"] / total) * 100;const transferPer = (row.data["timing-phase.transfer"] / total) * 100;const ttfbPer = (row.data["timing-phase.ttfb"] / total) * 100;const values = [connectionPer, dnsPer, tlPer, transferPer, ttfbPer];return (<div className="flex h-full w-full items-center px-4"><div className="flex h-4 w-full items-center gap-px overflow-hidden">{values.map((v, i) => {return (<divkey={i}style={{ width: `${v}%`, background: colors[i] }}className={clsx("h-full rounded-sm")}/>);})}</div></div>);}export function RowDetailRenderer({ row, grid }: RowDetailRendererParams<RequestData>) {// Guard against empty data.if (!grid.api.rowIsLeaf(row) || !row.data) return null;const total =row.data["timing-phase.connection"] +row.data["timing-phase.dns"] +row.data["timing-phase.tls"] +row.data["timing-phase.transfer"] +row.data["timing-phase.ttfb"];const connectionPer = (row.data["timing-phase.connection"] / total) * 100;const dnsPer = (row.data["timing-phase.dns"] / total) * 100;const tlPer = (row.data["timing-phase.tls"] / total) * 100;const transferPer = (row.data["timing-phase.transfer"] / total) * 100;const ttfbPer = (row.data["timing-phase.ttfb"] / total) * 100;return (<div className="flex h-full flex-col px-4 pb-[20px] pt-[7px] text-sm"><h3 className="text-ln-gray-60 mt-0 text-xs font-[500]">Timing Phases</h3><div className="flex flex-1 gap-2 pt-[6px]"><div className="bg-ln-gray-00 border-ln-gray-20 h-full flex-1 rounded-[10px] border"><div className="grid-cols[auto_auto_1fr] grid grid-rows-5 gap-1 gap-x-4 p-4 md:grid-cols-[auto_auto_200px_auto]"><TimingPhaseRowlabel="Transfer"color={colors[0]}msPercentage={transferPer}msValue={row.data["timing-phase.transfer"]}/><TimingPhaseRowlabel="DNS"color={colors[1]}msPercentage={dnsPer}msValue={row.data["timing-phase.dns"]}/><TimingPhaseRowlabel="Connection"color={colors[2]}msPercentage={connectionPer}msValue={row.data["timing-phase.connection"]}/><TimingPhaseRowlabel="TTFB"color={colors[3]}msPercentage={ttfbPer}msValue={row.data["timing-phase.ttfb"]}/><TimingPhaseRowlabel="TLS"color={colors[4]}msPercentage={tlPer}msValue={row.data["timing-phase.tls"]}/><div className="col-start-3 row-span-full flex h-full flex-1 items-center justify-center"><TimingPhasePieChart row={row.data} /></div></div></div></div></div>);}interface TimePhaseRowProps {readonly color: string;readonly msValue: number;readonly msPercentage: number;readonly label: string;}function TimingPhaseRow({ color, msValue, msPercentage, label }: TimePhaseRowProps) {return (<><div className="text-sm">{label}</div><div className="text-sm tabular-nums">{msPercentage.toFixed(2)}%</div><div className="col-start-4 hidden items-center justify-end gap-1 text-sm md:flex"><div><span className="text-ln-gray-100">{numberFormatter.format(msValue)}</span><span className="text-ln-gray-60 text-xs">ms</span></div><divclassName="rounded"style={{width: `${msValue}px`,height: "12px",background: color,display: "block",}}></div></div></>);}function TimingPhasePieChart({ row }: { row: RequestData }) {const data = useMemo(() => {return [{ subject: "Transfer", value: row["timing-phase.transfer"], color: colors[0] },{ subject: "DNS", value: row["timing-phase.dns"], color: colors[1] },{ subject: "Connection", value: row["timing-phase.connection"], color: colors[2] },{ subject: "TTFB", value: row["timing-phase.ttfb"], color: colors[3] },{ subject: "TLS", value: row["timing-phase.tls"], color: colors[4] },];}, [row]);return (<div style={{ height: 100 }}><PieChart data={data} startAngle={180} lengthAngle={180} center={[50, 75]} paddingAngle={1} /></div>);}
You can implement virtualization in several ways. LyteNyte Grid supports advanced scenarios such as variable row heights, row detail areas, cell and row spans, and pinned rows and columns. To learn more about general virtualization techniques, see this List Virtualization guide on patterns.dev.
Virtualization Configuration
You can customize or disable the grid's virtualization behavior. LyteNyte Grid renders all rows and columns visible in the viewport, along with a small number of rows and columns outside the viewport. These out-of-view elements form the grid's “overscan.” You can adjust overscan using four grid properties:
rowOverscanToprowOverscanBottomcolOverscanStartcolOverscanEnd
Since overscan affects only out-of-view content, changes may not appear obvious. To confirm overscan values, inspect the DOM and count the rendered rows outside the viewport. This is demonstrated in the example below:
Row Overscan Configuration
"use client";import "@1771technologies/lytenyte-pro/grid.css";import { Grid, useClientRowDataSource } from "@1771technologies/lytenyte-pro";import type { Column } from "@1771technologies/lytenyte-pro/types";import { bankDataSmall } from "@1771technologies/grid-sample-data/bank-data-smaller";import { useId } from "react";import { BalanceCell, DurationCell, NumberCell, tw } from "./components";type BankData = (typeof bankDataSmall)[number];const columns: Column<BankData>[] = [{ id: "job", width: 120 },{ id: "age", type: "number", width: 80, cellRenderer: NumberCell },{ id: "balance", type: "number", cellRenderer: BalanceCell },{ id: "education" },{ id: "marital" },{ id: "default" },{ id: "housing" },{ id: "loan" },{ id: "contact" },{ id: "day", type: "number", cellRenderer: NumberCell },{ id: "month" },{ id: "duration", type: "number", cellRenderer: DurationCell },{ id: "poutcome" },{ id: "y" },];export default function RowOverScan() {const ds = useClientRowDataSource({ data: bankDataSmall });const grid = Grid.useLyteNyte({gridId: useId(),rowDataSource: ds,columns,columnBase: { width: 100 },rowOverscanTop: 30,rowOverscanBottom: 30,});const view = grid.view.useValue();return (<div className="lng-grid" style={{ display: "flex", flexDirection: "column" }}><div style={{ height: 500 }}><Grid.Root grid={grid}><Grid.Viewport><Grid.Header>{view.header.layout.map((row, i) => {return (<Grid.HeaderRow key={i} headerRowIndex={i}>{row.map((c) => {if (c.kind === "group") return null;return (<Grid.HeaderCellkey={c.id}cell={c}className={tw("flex h-full w-full items-center px-2 text-sm capitalize",c.column.type === "number" && "justify-end",)}/>);})}</Grid.HeaderRow>);})}</Grid.Header><Grid.RowsContainer><Grid.RowsCenter>{view.rows.center.map((row) => {if (row.kind === "full-width") return null;return (<Grid.Row row={row} key={row.id}>{row.cells.map((c) => {return (<Grid.Cellkey={c.id}cell={c}className={tw("flex h-full w-full items-center px-2 text-sm",c.column.type === "number" && "justify-end",)}/>);})}</Grid.Row>);})}</Grid.RowsCenter></Grid.RowsContainer></Grid.Viewport></Grid.Root></div></div>);}
import { CheckIcon, MinusIcon } from "@radix-ui/react-icons";import type { ClassValue } from "clsx";import clsx from "clsx";import { Checkbox as C } from "radix-ui";import { twMerge } from "tailwind-merge";import type {CellRendererParams,HeaderCellRendererParams,} from "@1771technologies/lytenyte-pro/types";import type { bankDataSmall } from "@1771technologies/grid-sample-data/bank-data-smaller";import type { JSX } from "react";export type BankData = (typeof bankDataSmall)[number];const formatter = new Intl.NumberFormat("en-US", {maximumFractionDigits: 2,minimumFractionDigits: 0,});export function BalanceCell({ grid, row, column }: CellRendererParams<BankData>) {const field = grid.api.columnField(column, row);if (typeof field === "number") {if (field < 0) return `-$${formatter.format(Math.abs(field))}`;return "$" + formatter.format(field);}return `${field ?? "-"}`;}export function DurationCell({ grid, row, column }: CellRendererParams<BankData>) {const field = grid.api.columnField(column, row);return typeof field === "number" ? `${formatter.format(field)} days` : `${field ?? "-"}`;}export function NumberCell({ grid, row, column }: CellRendererParams<BankData>) {const field = grid.api.columnField(column, row);return typeof field === "number" ? formatter.format(field) : `${field ?? "-"}`;}export function MarkerHeader({ grid }: HeaderCellRendererParams<BankData>) {const allSelected = grid.state.rowDataSource.get().rowAreAllSelected();const selected = grid.state.rowSelectedIds.useValue();return (<div className="flex h-full w-full items-center justify-center"><GridCheckboxchecked={allSelected || selected.size > 0}indeterminate={!allSelected && selected.size > 0}onClick={(ev) => {ev.preventDefault();grid.api.rowSelectAll({ deselect: allSelected });}}onKeyDown={(ev) => {if (ev.key === "Enter" || ev.key === " ")grid.api.rowSelectAll({ deselect: allSelected });}}/></div>);}export function MarkerCell({ grid, rowSelected }: CellRendererParams<BankData>) {return (<div className="flex h-full w-full items-center justify-center"><GridCheckboxchecked={rowSelected}onClick={(ev) => {ev.stopPropagation();grid.api.rowHandleSelect({ shiftKey: ev.shiftKey, target: ev.target });}}onKeyDown={(ev) => {if (ev.key === "Enter" || ev.key === " ")grid.api.rowHandleSelect({ shiftKey: ev.shiftKey, target: ev.target });}}/></div>);}export function GridCheckbox({children,indeterminate,...props}: C.CheckboxProps & { indeterminate?: boolean }) {return (<label className="text-md text-light flex items-center gap-2"><C.Root{...props}type="button"className={tw("bg-ln-gray-02 rounded border-transparent","shadow-[0_1.5px_2px_0_rgba(18,46,88,0.08),0_0_0_1px_var(--lng1771-gray-40)]","data-[state=checked]:bg-ln-primary-50 data-[state=checked]:shadow-[0_1.5px_2px_0_rgba(18,46,88,0.08),0_0_0_1px_var(--lng1771-primary-50)]","h-4 w-4",props.className,)}><C.CheckboxIndicator className={tw("flex items-center justify-center")}>{!indeterminate && <CheckIcon className="text-white dark:text-black" />}{indeterminate && <MinusIcon className="text-white dark:text-black" />}</C.CheckboxIndicator></C.Root>{children}</label>);}export function tw(...c: ClassValue[]) {return twMerge(clsx(...c));}export function GridButton(props: JSX.IntrinsicElements["button"]) {return (<button{...props}className={tw("border-ln-gray-30 hover:bg-ln-gray-80 dark:hover:bg-ln-gray-90 flex h-8 cursor-pointer items-center gap-2 rounded-lg border bg-black px-2 text-sm text-white shadow transition-colors dark:bg-white dark:text-black",props.className,)}></button>);}
In this example, the grid renders 30 extra rows above and below the viewport. The configuration looks as follows:
const grid = Grid.useLyteNyte({// Other propertiesrowOverscanTop: 30,rowOverscanBottom: 30,});
Disabling Virtualization
If your dataset is small (fewer than ~100 rows), you may disable virtualization entirely. Use the
virtualizeRows and virtualizeCols properties to turn virtualization off.
Disabling Virtualization
"use client";import "@1771technologies/lytenyte-pro/grid.css";import { Grid, useClientRowDataSource } from "@1771technologies/lytenyte-pro";import type { Column } from "@1771technologies/lytenyte-pro/types";import { bankDataSmall } from "@1771technologies/grid-sample-data/bank-data-smaller";import { useId } from "react";import { BalanceCell, NumberCell, tw } from "./components";type BankData = (typeof bankDataSmall)[number];const columns: Column<BankData>[] = [{ id: "job", width: 120 },{ id: "age", type: "number", width: 80, cellRenderer: NumberCell },{ id: "balance", type: "number", cellRenderer: BalanceCell },{ id: "education" },{ id: "marital" },{ id: "default" },{ id: "housing" },{ id: "loan" },{ id: "contact" },{ id: "day", type: "number", cellRenderer: NumberCell },{ id: "month" },];export default function RowOverScan() {const ds = useClientRowDataSource({ data: bankDataSmall.slice(0, 40) });const grid = Grid.useLyteNyte({gridId: useId(),rowDataSource: ds,columns,columnBase: { width: 100 },virtualizeCols: false,virtualizeRows: false,});const view = grid.view.useValue();return (<div className="lng-grid" style={{ display: "flex", flexDirection: "column" }}><div style={{ height: 500 }}><Grid.Root grid={grid}><Grid.Viewport><Grid.Header>{view.header.layout.map((row, i) => {return (<Grid.HeaderRow key={i} headerRowIndex={i}>{row.map((c) => {if (c.kind === "group") return null;return (<Grid.HeaderCellkey={c.id}cell={c}className={tw("flex h-full w-full items-center px-2 text-sm capitalize",c.column.type === "number" && "justify-end",)}/>);})}</Grid.HeaderRow>);})}</Grid.Header><Grid.RowsContainer><Grid.RowsCenter>{view.rows.center.map((row) => {if (row.kind === "full-width") return null;return (<Grid.Row row={row} key={row.id}>{row.cells.map((c) => {return (<Grid.Cellkey={c.id}cell={c}className={tw("flex h-full w-full items-center px-2 text-sm",c.column.type === "number" && "justify-end",)}/>);})}</Grid.Row>);})}</Grid.RowsCenter></Grid.RowsContainer></Grid.Viewport></Grid.Root></div></div>);}
import { CheckIcon, MinusIcon } from "@radix-ui/react-icons";import type { ClassValue } from "clsx";import clsx from "clsx";import { Checkbox as C } from "radix-ui";import { twMerge } from "tailwind-merge";import type {CellRendererParams,HeaderCellRendererParams,} from "@1771technologies/lytenyte-pro/types";import type { bankDataSmall } from "@1771technologies/grid-sample-data/bank-data-smaller";import type { JSX } from "react";export type BankData = (typeof bankDataSmall)[number];const formatter = new Intl.NumberFormat("en-US", {maximumFractionDigits: 2,minimumFractionDigits: 0,});export function BalanceCell({ grid, row, column }: CellRendererParams<BankData>) {const field = grid.api.columnField(column, row);if (typeof field === "number") {if (field < 0) return `-$${formatter.format(Math.abs(field))}`;return "$" + formatter.format(field);}return `${field ?? "-"}`;}export function DurationCell({ grid, row, column }: CellRendererParams<BankData>) {const field = grid.api.columnField(column, row);return typeof field === "number" ? `${formatter.format(field)} days` : `${field ?? "-"}`;}export function NumberCell({ grid, row, column }: CellRendererParams<BankData>) {const field = grid.api.columnField(column, row);return typeof field === "number" ? formatter.format(field) : `${field ?? "-"}`;}export function MarkerHeader({ grid }: HeaderCellRendererParams<BankData>) {const allSelected = grid.state.rowDataSource.get().rowAreAllSelected();const selected = grid.state.rowSelectedIds.useValue();return (<div className="flex h-full w-full items-center justify-center"><GridCheckboxchecked={allSelected || selected.size > 0}indeterminate={!allSelected && selected.size > 0}onClick={(ev) => {ev.preventDefault();grid.api.rowSelectAll({ deselect: allSelected });}}onKeyDown={(ev) => {if (ev.key === "Enter" || ev.key === " ")grid.api.rowSelectAll({ deselect: allSelected });}}/></div>);}export function MarkerCell({ grid, rowSelected }: CellRendererParams<BankData>) {return (<div className="flex h-full w-full items-center justify-center"><GridCheckboxchecked={rowSelected}onClick={(ev) => {ev.stopPropagation();grid.api.rowHandleSelect({ shiftKey: ev.shiftKey, target: ev.target });}}onKeyDown={(ev) => {if (ev.key === "Enter" || ev.key === " ")grid.api.rowHandleSelect({ shiftKey: ev.shiftKey, target: ev.target });}}/></div>);}export function GridCheckbox({children,indeterminate,...props}: C.CheckboxProps & { indeterminate?: boolean }) {return (<label className="text-md text-light flex items-center gap-2"><C.Root{...props}type="button"className={tw("bg-ln-gray-02 rounded border-transparent","shadow-[0_1.5px_2px_0_rgba(18,46,88,0.08),0_0_0_1px_var(--lng1771-gray-40)]","data-[state=checked]:bg-ln-primary-50 data-[state=checked]:shadow-[0_1.5px_2px_0_rgba(18,46,88,0.08),0_0_0_1px_var(--lng1771-primary-50)]","h-4 w-4",props.className,)}><C.CheckboxIndicator className={tw("flex items-center justify-center")}>{!indeterminate && <CheckIcon className="text-white dark:text-black" />}{indeterminate && <MinusIcon className="text-white dark:text-black" />}</C.CheckboxIndicator></C.Root>{children}</label>);}export function tw(...c: ClassValue[]) {return twMerge(clsx(...c));}export function GridButton(props: JSX.IntrinsicElements["button"]) {return (<button{...props}className={tw("border-ln-gray-30 hover:bg-ln-gray-80 dark:hover:bg-ln-gray-90 flex h-8 cursor-pointer items-center gap-2 rounded-lg border bg-black px-2 text-sm text-white shadow transition-colors dark:bg-white dark:text-black",props.className,)}></button>);}
In the demo, the dataset includes only 40 rows and a limited number of columns. Rendering everything at once reduces performance for larger datasets, which holds true for all data grids, not just LyteNyte Grid.
You may need to disable virtualization when printing the grid. Because virtualization removes out-of-view rows and columns from the DOM, printed output includes only the rendered content. Turning off virtualization ensures the full dataset appears in the printed result.
Virtualization Considerations
Virtualization significantly improves performance, but it introduces several important considerations:
- Grid State: Since the grid mounts and unmounts row and cell components as they enter and exit the viewport, any React state stored inside a cell component disappears when that component unmounts. Therefore, it's important to maintain any required state higher in the component tree.
- DOM Virtualization: Virtualized rows and columns do not exist in the DOM. Browser-level “find in page” cannot match cells outside the viewport, so any DOM queries only return rendered elements.
- Scroll Speed: Rapid scrolling, such as dragging the scrollbar to the bottom, may reveal a momentary background flash before new content appears. This effect occurs because scrolling runs on a separate thread in modern browsers. The scroll position updates before the main thread paints the new content.
Some grids mitigate flashes during rapid scrolling by translating content on the main thread, but that approach causes frame drops and reduces overall application performance.
Next Steps
- Headless Component Parts: Learn about the component parts that make up LyteNyte Grid.
- Grid Events: Learn how to handle events fired by LyteNyte Grid and different approaches to event handling.
- Grid Atoms & Reactivity: Learn how to manage reactivity and state changes in LyteNyte Grid.
Grid Events
This guide explains how to listen for and handle grid events. LyteNyte Grid fires events in response to specific user interactions or when imperative API methods run.
Grid Theming
LyteNyte Grid is a headless data grid. This guide explains how to theme LyteNyte Grid using vanilla CSS or pre-built themes to create a visually polished data grid.