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
24 collapsed lines
1import "./main.css";2import "@1771technologies/lytenyte-pro/light-dark.css";3import {4 Grid,5 useClientDataSource,6 type UseClientDataSourceParams,7} from "@1771technologies/lytenyte-pro";8
9import type { RequestData } from "./data.js";10import { requestData } from "./data.js";11import {12 DateCell,13 Header,14 LatencyCell,15 MarkerCell,16 MethodCell,17 PathnameCell,18 RegionCell,19 RowDetailRenderer,20 StatusCell,21 TimingPhaseCell,22} from "./components.js";23import { useMemo, useState } from "react";24import { sortComparators } from "./comparators.js";25
26export interface GridSpec {27 data: RequestData;28 column: { sort?: "asc" | "desc" | null };29}30
31const base: Grid.Props<GridSpec>["columnBase"] = {32 headerRenderer: Header,33};34
35const marker: Grid.Props<GridSpec>["columnMarker"] = {36 on: true,37 width: 40,38 headerRenderer: () => <div className="sr-only">Toggle row detail expansion</div>,39 cellRenderer: MarkerCell,40};41
42export default function GettingStartedDemo() {43 const [columns, setColumns] = useState<Grid.Column<GridSpec>[]>([44 { id: "Date", name: "Date", width: 200, type: "datetime", cellRenderer: DateCell },45 { id: "Status", name: "Status", width: 100, cellRenderer: StatusCell },46 { id: "Method", name: "Method", width: 100, cellRenderer: MethodCell },47 { id: "timing-phase", name: "Timing Phase", cellRenderer: TimingPhaseCell },48 { id: "Pathname", name: "Pathname", cellRenderer: PathnameCell },49 { id: "Latency", name: "Latency", width: 120, type: "number", cellRenderer: LatencyCell },50 { id: "region", name: "Region", cellRenderer: RegionCell },51 ]);52
53 const sort = useMemo<UseClientDataSourceParams<GridSpec>["sort"]>(() => {54 const colWithSort = columns.find((x) => x.sort);55 if (!colWithSort) return null;56
57 if (sortComparators[colWithSort.id])58 return [{ dim: sortComparators[colWithSort.id], descending: colWithSort.sort === "desc" }];59
60 return [{ dim: colWithSort, descending: colWithSort.sort === "desc" }];61 }, [columns]);62
63 const ds = useClientDataSource<GridSpec>({64 data: requestData,65 sort,66 });67
68 return (69 <div className="demo ln-grid" style={{ height: 400 }}>70 <Grid71 columns={columns}72 onColumnsChange={setColumns}73 columnBase={base}74 rowSource={ds}75 rowDetailRenderer={RowDetailRenderer}76 columnMarker={marker}77 />78 </div>79 );80}1import type { GridSpec } from "./demo";2import { useMemo } from "react";3import { format } from "date-fns";4import clsx from "clsx";5import type { RequestData } from "./data";6import { PieChart } from "react-minimal-pie-chart";7import { ArrowDownIcon, ArrowUpIcon, ChevronDownIcon, ChevronRightIcon } from "@radix-ui/react-icons";8import type { Grid } from "@1771technologies/lytenyte-pro";9
10export function Header({ api, column }: Grid.T.HeaderParams<GridSpec>) {11 return (12 <div13 className="text-ln-gray-60 group relative flex h-full w-full cursor-pointer items-center px-1 text-sm transition-colors"14 onClick={() => {15 const columns = api.props().columns;16 if (!columns) return;17
18 const updates: Record<string, Partial<Grid.Column<GridSpec>>> = {};19 const columnWithSort = columns.filter((x) => x.sort);20 columnWithSort.forEach((x) => {21 updates[x.id] = { sort: null };22 });23
24 if (column.sort === "asc") {25 updates[column.id] = { sort: null };26 } else if (column.sort === "desc") {27 updates[column.id] = { sort: "asc" };28 } else {29 updates[column.id] = { sort: "desc" };30 }31
32 api.columnUpdate(updates);33 }}34 >35 <div className="sort-button flex w-full items-center justify-between rounded px-1 py-1 transition-colors">36 {column.name ?? column.id}37
38 {column.sort === "asc" && <ArrowUpIcon className="text-ln-text-dark size-4" />}39 {column.sort === "desc" && <ArrowDownIcon className="text-ln-text-dark size-4" />}40 </div>41 </div>42 );43}44
45export function MarkerCell({ detailExpanded, row, api }: Grid.T.CellRendererParams<GridSpec>) {46 return (47 <button48 className="text-ln-text flex h-full w-[calc(100%-1px)] cursor-pointer items-center justify-center"49 onClick={() => api.rowDetailToggle(row)}50 >51 {detailExpanded ? (52 <ChevronDownIcon width={20} height={20} />53 ) : (54 <ChevronRightIcon width={20} height={20} />55 )}56 </button>57 );58}59
60export function DateCell({ column, row, api }: Grid.T.CellRendererParams<GridSpec>) {61 const field = api.columnField(column, row);62
63 const niceDate = useMemo(() => {64 if (typeof field !== "string") return null;65 return format(field, "MMM dd, yyyy HH:mm:ss");66 }, [field]);67
68 // Guard against bad values and render nothing69 if (!niceDate) return null;70
71 return <div className="text-ln-text flex h-full w-full items-center tabular-nums">{niceDate}</div>;72}73
74export function StatusCell({ column, row, api }: Grid.T.CellRendererParams<GridSpec>) {75 const status = api.columnField(column, row);76
77 // Guard against bad values78 if (typeof status !== "number") return null;79
80 return (81 <div className={clsx("flex h-full w-full items-center text-xs font-bold")}>82 <div83 className={clsx(84 "rounded-sm px-1 py-px",85 status < 400 && "text-ln-primary-50 bg-[#126CFF1F]",86 status >= 400 && status < 500 && "bg-[#FF991D1C] text-[#EEA760]",87 status >= 500 && "bg-[#e63d3d2d] text-[#e63d3d]",88 )}89 >90 {status}91 </div>92 </div>93 );94}95
96export function MethodCell({ column, row, api }: Grid.T.CellRendererParams<GridSpec>) {97 const method = api.columnField(column, row);98
99 // Guard against bad values100 if (typeof method !== "string") return null;101
102 return (103 <div className={clsx("flex h-full w-full items-center text-xs font-bold")}>104 <div105 className={clsx(106 "rounded-sm px-1 py-px",107 method === "GET" && "text-ln-primary-50 bg-[#126CFF1F]",108 (method === "PATCH" || method === "PUT" || method === "POST") && "bg-[#FF991D1C] text-[#EEA760]",109 method === "DELETE" && "bg-[#e63d3d2d] text-[#e63d3d]",110 )}111 >112 {method}113 </div>114 </div>115 );116}117
118export function PathnameCell({ column, row, api }: Grid.T.CellRendererParams<GridSpec>) {119 const path = api.columnField(column, row);120
121 if (typeof path !== "string") return null;122
123 return (124 <div className="text-ln-text-dark flex h-full w-full items-center text-sm">125 <div className="text-ln-primary-50 w-full overflow-hidden text-ellipsis text-nowrap">{path}</div>126 </div>127 );128}129
130const numberFormatter = new Intl.NumberFormat("en-Us", {131 maximumFractionDigits: 0,132 minimumFractionDigits: 0,133});134export function LatencyCell({ column, row, api }: Grid.T.CellRendererParams<GridSpec>) {135 const ms = api.columnField(column, row);136 if (typeof ms !== "number") return null;137
138 return (139 <div className="text-ln-text-dark flex h-full w-full items-center text-sm tabular-nums">140 <div>141 <span className="text-ln-gray-100">{numberFormatter.format(ms)}</span>142 <span className="text-ln-text-light text-xs">ms</span>143 </div>144 </div>145 );146}147
148export function RegionCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {149 // Only render for leaf rows and we have some data150 if (!api.rowIsLeaf(row) || !row.data) return null;151
152 const shortName = row.data["region.shortname"];153 const longName = row.data["region.fullname"];154
155 return (156 <div className="flex h-full w-full items-center">157 <div className="flex items-baseline gap-2 text-sm">158 <div className="text-ln-gray-100">{shortName}</div>159 <div className="text-ln-text-light leading-4">{longName}</div>160 </div>161 </div>162 );163}164
165const colors = ["var(--transfer)", "var(--dns)", "var(--connection)", "var(--ttfb)", "var(--tls)"];166export function TimingPhaseCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {167 // Guard against rows that are not leafs or rows that have no data.168 if (!api.rowIsLeaf(row) || !row.data) return;169
170 const total =171 row.data["timing-phase.connection"] +172 row.data["timing-phase.dns"] +173 row.data["timing-phase.tls"] +174 row.data["timing-phase.transfer"] +175 row.data["timing-phase.ttfb"];176
177 const connectionPer = (row.data["timing-phase.connection"] / total) * 100;178 const dnsPer = (row.data["timing-phase.dns"] / total) * 100;179 const tlPer = (row.data["timing-phase.tls"] / total) * 100;180 const transferPer = (row.data["timing-phase.transfer"] / total) * 100;181 const ttfbPer = (row.data["timing-phase.ttfb"] / total) * 100;182
183 const values = [connectionPer, dnsPer, tlPer, transferPer, ttfbPer];184
185 return (186 <div className="flex h-full w-full items-center">187 <div className="flex h-4 w-full items-center gap-px overflow-hidden">188 {values.map((v, i) => {189 return (190 <div191 key={i}192 style={{ width: `${v}%`, background: colors[i] }}193 className={clsx("h-full rounded-sm")}194 />195 );196 })}197 </div>198 </div>199 );200}201
202export function RowDetailRenderer({ row, api }: Grid.T.RowParams<GridSpec>) {203 // Guard against empty data.204 if (!api.rowIsLeaf(row) || !row.data) return null;205
206 const total =207 row.data["timing-phase.connection"] +208 row.data["timing-phase.dns"] +209 row.data["timing-phase.tls"] +210 row.data["timing-phase.transfer"] +211 row.data["timing-phase.ttfb"];212
213 const connectionPer = (row.data["timing-phase.connection"] / total) * 100;214 const dnsPer = (row.data["timing-phase.dns"] / total) * 100;215 const tlPer = (row.data["timing-phase.tls"] / total) * 100;216 const transferPer = (row.data["timing-phase.transfer"] / total) * 100;217 const ttfbPer = (row.data["timing-phase.ttfb"] / total) * 100;218
219 return (220 <div className="pt-1.75 flex h-full flex-col px-4 pb-5 text-sm">221 <h3 className="text-ln-text-xlight mt-0 text-xs font-medium">Timing Phases</h3>222
223 <div className="flex flex-1 gap-2 pt-1.5">224 <div className="bg-ln-gray-00 border-ln-gray-20 h-full flex-1 rounded-[10px] border">225 <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]">226 <TimingPhaseRow227 label="Transfer"228 color={colors[0]}229 msPercentage={transferPer}230 msValue={row.data["timing-phase.transfer"]}231 />232 <TimingPhaseRow233 label="DNS"234 color={colors[1]}235 msPercentage={dnsPer}236 msValue={row.data["timing-phase.dns"]}237 />238 <TimingPhaseRow239 label="Connection"240 color={colors[2]}241 msPercentage={connectionPer}242 msValue={row.data["timing-phase.connection"]}243 />244 <TimingPhaseRow245 label="TTFB"246 color={colors[3]}247 msPercentage={ttfbPer}248 msValue={row.data["timing-phase.ttfb"]}249 />250 <TimingPhaseRow251 label="TLS"252 color={colors[4]}253 msPercentage={tlPer}254 msValue={row.data["timing-phase.tls"]}255 />256
257 <div className="col-start-3 row-span-full flex h-full flex-1 items-center justify-center">258 <TimingPhasePieChart row={row.data} />259 </div>260 </div>261 </div>262 </div>263 </div>264 );265}266
267interface TimePhaseRowProps {268 readonly color: string;269 readonly msValue: number;270 readonly msPercentage: number;271 readonly label: string;272}273
274function TimingPhaseRow({ color, msValue, msPercentage, label }: TimePhaseRowProps) {275 return (276 <>277 <div className="text-sm">{label}</div>278 <div className="text-sm tabular-nums">{msPercentage.toFixed(2)}%</div>279 <div className="col-start-4 hidden items-center justify-end gap-1 text-sm md:flex">280 <div>281 <span className="text-ln-gray-100">{numberFormatter.format(msValue)}</span>282 <span className="text-ln-text-xlight text-xs">ms</span>283 </div>284 <div285 className="rounded"286 style={{287 width: `${msValue}px`,288 height: "12px",289 background: color,290 display: "block",291 }}292 ></div>293 </div>294 </>295 );296}297
298function TimingPhasePieChart({ row }: { row: RequestData }) {299 const data = useMemo(() => {300 return [301 { subject: "Transfer", value: row["timing-phase.transfer"], color: colors[0] },302 { subject: "DNS", value: row["timing-phase.dns"], color: colors[1] },303 { subject: "Connection", value: row["timing-phase.connection"], color: colors[2] },304 { subject: "TTFB", value: row["timing-phase.ttfb"], color: colors[3] },305 { subject: "TLS", value: row["timing-phase.tls"], color: colors[4] },306 ];307 }, [row]);308
309 return (310 <div style={{ height: 100 }}>311 <PieChart data={data} startAngle={180} lengthAngle={180} center={[50, 75]} paddingAngle={1} />312 </div>313 );314}1import type { GridSpec } from "./demo.js";2import type { RequestData } from "./data.js";3import { compareAsc } from "date-fns";4import type { Grid } from "@1771technologies/lytenyte-pro";5
6export const sortComparators: Record<string, Grid.T.SortFn<GridSpec["data"]>> = {7 region: (left, right) => {8 if (left.kind !== "leaf" && right.kind !== "leaf") return 0;9 if (left.kind === "leaf" && right.kind !== "leaf") return -1;10 if (left.kind !== "leaf" && right.kind === "leaf") return 1;11
12 const leftData = left.data as RequestData;13 const rightData = right.data as RequestData;14
15 return leftData["region.fullname"].localeCompare(rightData["region.fullname"]);16 },17 "timing-phase": (left, right) => {18 if (left.kind !== "leaf" && right.kind !== "leaf") return 0;19 if (left.kind === "leaf" && right.kind !== "leaf") return -1;20 if (left.kind !== "leaf" && right.kind === "leaf") return 1;21
22 const leftData = left.data as RequestData;23 const rightData = right.data as RequestData;24
25 return leftData.Latency - rightData.Latency;26 },27 Date: (left, right) => {28 if (left.kind !== "leaf" && right.kind !== "leaf") return 0;29 if (left.kind === "leaf" && right.kind !== "leaf") return -1;30 if (left.kind !== "leaf" && right.kind === "leaf") return 1;31
32 const leftData = left.data as RequestData;33 const rightData = right.data as RequestData;34
35 return compareAsc(leftData.Date, rightData.Date);36 },37};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
1import "@1771technologies/lytenyte-pro/components.css";2import "@1771technologies/lytenyte-pro/light-dark.css";3import { salesData, type SaleDataItem } from "@1771technologies/grid-sample-data/sales-data";4import { Grid, useClientDataSource } from "@1771technologies/lytenyte-pro";5import {6 AgeGroup,7 CostCell,8 CountryCell,9 DateCell,10 GenderCell,11 NumberCell,12 ProfitCell,13} from "./components.js";14
15export interface GridSpec {16 readonly data: SaleDataItem;17}18
19export const columns: Grid.Column<GridSpec>[] = [20 { id: "date", name: "Date", cellRenderer: DateCell, width: 110 },21 { id: "age", name: "Age", type: "number", width: 80 },22 { id: "ageGroup", name: "Age Group", cellRenderer: AgeGroup, width: 160 },23 { id: "customerGender", name: "Gender", cellRenderer: GenderCell, width: 100 },24 { id: "country", name: "Country", cellRenderer: CountryCell, width: 150 },25 { id: "orderQuantity", name: "Quantity", type: "number", width: 60 },26 { id: "unitPrice", name: "Price", type: "number", width: 80, cellRenderer: NumberCell },27 { id: "cost", name: "Cost", width: 80, type: "number", cellRenderer: CostCell },28 { id: "revenue", name: "Revenue", width: 80, type: "number", cellRenderer: ProfitCell },29 { id: "profit", name: "Profit", width: 80, type: "number", cellRenderer: ProfitCell },30 { id: "state", name: "State", width: 150 },31 { id: "product", name: "Product", width: 160 },32 { id: "productCategory", name: "Category", width: 120 },33 { id: "subCategory", name: "Sub-Category", width: 160 },34];35
36const base: Grid.ColumnBase<GridSpec> = { width: 120 };37
38export default function GridDemo() {39 const ds = useClientDataSource<GridSpec>({40 data: salesData,41 });42
43 return (44 <div className="ln-grid" style={{ height: 500 }}>45 <Grid columns={columns} columnBase={base} rowSource={ds} rowOverscanTop={30} rowOverscanBottom={30} />46 </div>47 );48}1import type { Grid } from "@1771technologies/lytenyte-pro";2import type { GridSpec } from "./demo";3import { format, isValid, parse } from "date-fns";4import { countryFlags } from "@1771technologies/grid-sample-data/sales-data";5import { clsx, type ClassValue } from "clsx";6import { twMerge } from "tailwind-merge";7
8export function DateCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {9 const field = api.columnField(column, row);10
11 if (typeof field !== "string") return "-";12
13 const dateField = parse(field as string, "MM/dd/yyyy", new Date());14
15 if (!isValid(dateField)) return "-";16
17 const niceDate = format(dateField, "yyyy MMM dd");18 return <div className="flex h-full w-full items-center tabular-nums">{niceDate}</div>;19}20
21export function AgeGroup({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {22 const field = api.columnField(column, row);23
24 if (field === "Youth (<25)") return <div className="text-[#944cec] dark:text-[#B181EB]">{field}</div>;25 if (field === "Young Adults (25-34)")26 return <div className="text-[#aa6c1a] dark:text-[#E5B474]">{field}</div>;27 if (field === "Adults (35-64)") return <div className="text-[#0f7d4c] dark:text-[#52B086]">{field}</div>;28
29 return "-";30}31
32export function GenderCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {33 const field = api.columnField(column, row);34
35 if (field === "Male")36 return (37 <div className="flex h-full w-full items-center gap-2">38 <div className="flex size-6 items-center justify-center rounded-full bg-blue-500/50">39 <span className="iconify ph--gender-male-bold size-4" />40 </div>41 Male42 </div>43 );44
45 if (field === "Female")46 return (47 <div className="flex h-full w-full items-center gap-2">48 <div className="flex size-6 items-center justify-center rounded-full bg-pink-500/50">49 <span className="iconify ph--gender-female-bold size-4" />50 </div>51 Female52 </div>53 );54
55 return "-";56}57
58function tw(...c: ClassValue[]) {59 return twMerge(clsx(...c));60}61
62const formatter = new Intl.NumberFormat("en-US", {63 maximumFractionDigits: 2,64 minimumFractionDigits: 0,65});66export function ProfitCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {67 const field = api.columnField(column, row);68
69 if (typeof field !== "number") return "-";70
71 const formatted = field < 0 ? `-$${formatter.format(Math.abs(field))}` : "$" + formatter.format(field);72
73 return (74 <div75 className={tw(76 "flex h-full w-full items-center justify-end tabular-nums",77 field < 0 && "text-red-600 dark:text-red-300",78 field > 0 && "text-green-600 dark:text-green-300",79 )}80 >81 {formatted}82 </div>83 );84}85
86export function NumberCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {87 const field = api.columnField(column, row);88
89 if (typeof field !== "number") return "-";90
91 const formatted = field < 0 ? `-$${formatter.format(Math.abs(field))}` : "$" + formatter.format(field);92
93 return <div className={"flex h-full w-full items-center justify-end tabular-nums"}>{formatted}</div>;94}95
96export function CostCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {97 const field = api.columnField(column, row);98
99 if (typeof field !== "number") return "-";100
101 const formatted = field < 0 ? `-$${formatter.format(Math.abs(field))}` : "$" + formatter.format(field);102
103 return (104 <div105 className={tw(106 "flex h-full w-full items-center justify-end tabular-nums text-red-600 dark:text-red-300",107 )}108 >109 {formatted}110 </div>111 );112}113
114export function CountryCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {115 const field = api.columnField(column, row);116
117 const flag = countryFlags[field as keyof typeof countryFlags];118 if (!flag) return "-";119
120 return (121 <div className="flex h-full w-full items-center gap-2">122 <img className="size-4" src={flag} alt={`country flag of ${field}`} />123 <span>{String(field ?? "-")}</span>124 </div>125 );126}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
1import "@1771technologies/lytenyte-pro/components.css";2import "@1771technologies/lytenyte-pro/light-dark.css";3import { salesData, type SaleDataItem } from "@1771technologies/grid-sample-data/sales-data";4import { Grid, useClientDataSource } from "@1771technologies/lytenyte-pro";5import {6 AgeGroup,7 CostCell,8 CountryCell,9 DateCell,10 GenderCell,11 NumberCell,12 ProfitCell,13} from "./components.js";14
15export interface GridSpec {16 readonly data: SaleDataItem;17}18
19export const columns: Grid.Column<GridSpec>[] = [20 { id: "date", name: "Date", cellRenderer: DateCell, width: 110 },21 { id: "age", name: "Age", type: "number", width: 80 },22 { id: "ageGroup", name: "Age Group", cellRenderer: AgeGroup, width: 160 },23 { id: "customerGender", name: "Gender", cellRenderer: GenderCell, width: 100 },24 { id: "country", name: "Country", cellRenderer: CountryCell, width: 150 },25 { id: "orderQuantity", name: "Quantity", type: "number", width: 60 },26 { id: "unitPrice", name: "Price", type: "number", width: 80, cellRenderer: NumberCell },27 { id: "cost", name: "Cost", width: 80, type: "number", cellRenderer: CostCell },28 { id: "revenue", name: "Revenue", width: 80, type: "number", cellRenderer: ProfitCell },29 { id: "profit", name: "Profit", width: 80, type: "number", cellRenderer: ProfitCell },30 { id: "state", name: "State", width: 150 },31 { id: "product", name: "Product", width: 160 },32 { id: "productCategory", name: "Category", width: 120 },33 { id: "subCategory", name: "Sub-Category", width: 160 },34];35
36const base: Grid.ColumnBase<GridSpec> = { width: 120 };37
38export default function GridDemo() {39 const ds = useClientDataSource<GridSpec>({40 data: salesData.slice(0, 30),41 });42
43 return (44 <div className="ln-grid" style={{ height: 500 }}>45 <Grid46 columns={columns}47 columnBase={base}48 rowSource={ds}49 virtualizeRows={false}50 virtualizeCols={false}51 />52 </div>53 );54}1import type { Grid } from "@1771technologies/lytenyte-pro";2import type { GridSpec } from "./demo";3import { format, isValid, parse } from "date-fns";4import { countryFlags } from "@1771technologies/grid-sample-data/sales-data";5import { clsx, type ClassValue } from "clsx";6import { twMerge } from "tailwind-merge";7
8export function DateCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {9 const field = api.columnField(column, row);10
11 if (typeof field !== "string") return "-";12
13 const dateField = parse(field as string, "MM/dd/yyyy", new Date());14
15 if (!isValid(dateField)) return "-";16
17 const niceDate = format(dateField, "yyyy MMM dd");18 return <div className="flex h-full w-full items-center tabular-nums">{niceDate}</div>;19}20
21export function AgeGroup({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {22 const field = api.columnField(column, row);23
24 if (field === "Youth (<25)") return <div className="text-[#944cec] dark:text-[#B181EB]">{field}</div>;25 if (field === "Young Adults (25-34)")26 return <div className="text-[#aa6c1a] dark:text-[#E5B474]">{field}</div>;27 if (field === "Adults (35-64)") return <div className="text-[#0f7d4c] dark:text-[#52B086]">{field}</div>;28
29 return "-";30}31
32export function GenderCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {33 const field = api.columnField(column, row);34
35 if (field === "Male")36 return (37 <div className="flex h-full w-full items-center gap-2">38 <div className="flex size-6 items-center justify-center rounded-full bg-blue-500/50">39 <span className="iconify ph--gender-male-bold size-4" />40 </div>41 Male42 </div>43 );44
45 if (field === "Female")46 return (47 <div className="flex h-full w-full items-center gap-2">48 <div className="flex size-6 items-center justify-center rounded-full bg-pink-500/50">49 <span className="iconify ph--gender-female-bold size-4" />50 </div>51 Female52 </div>53 );54
55 return "-";56}57
58function tw(...c: ClassValue[]) {59 return twMerge(clsx(...c));60}61
62const formatter = new Intl.NumberFormat("en-US", {63 maximumFractionDigits: 2,64 minimumFractionDigits: 0,65});66export function ProfitCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {67 const field = api.columnField(column, row);68
69 if (typeof field !== "number") return "-";70
71 const formatted = field < 0 ? `-$${formatter.format(Math.abs(field))}` : "$" + formatter.format(field);72
73 return (74 <div75 className={tw(76 "flex h-full w-full items-center justify-end tabular-nums",77 field < 0 && "text-red-600 dark:text-red-300",78 field > 0 && "text-green-600 dark:text-green-300",79 )}80 >81 {formatted}82 </div>83 );84}85
86export function NumberCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {87 const field = api.columnField(column, row);88
89 if (typeof field !== "number") return "-";90
91 const formatted = field < 0 ? `-$${formatter.format(Math.abs(field))}` : "$" + formatter.format(field);92
93 return <div className={"flex h-full w-full items-center justify-end tabular-nums"}>{formatted}</div>;94}95
96export function CostCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {97 const field = api.columnField(column, row);98
99 if (typeof field !== "number") return "-";100
101 const formatted = field < 0 ? `-$${formatter.format(Math.abs(field))}` : "$" + formatter.format(field);102
103 return (104 <div105 className={tw(106 "flex h-full w-full items-center justify-end tabular-nums text-red-600 dark:text-red-300",107 )}108 >109 {formatted}110 </div>111 );112}113
114export function CountryCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {115 const field = api.columnField(column, row);116
117 const flag = countryFlags[field as keyof typeof countryFlags];118 if (!flag) return "-";119
120 return (121 <div className="flex h-full w-full items-center gap-2">122 <img className="size-4" src={flag} alt={`country flag of ${field}`} />123 <span>{String(field ?? "-")}</span>124 </div>125 );126}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.
Note
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
- Responsive Container: Learn how to configure containers for LyteNyte Grid.
- Headless Component Parts: Learn about the component parts that make up LyteNyte Grid.
- Getting Started: Get started with LyteNyte Grid, a modern React data grid designed for enterprise-scale data challenges.
