Skip to main content

useGridVirtualization

Render only the rows (and optionally columns) in view. You own the scroll container; the hook watches its scroll position and size, and returns the visible index range plus the pixel offsets for spacer elements above and below. It uses core's pure compute helpers and plain DOM events, with no third-party dependency.

Live demo​

10,000 rows in a plain <table>; only the visible window is in the DOM. Scroll, sort, or jump
Live

Quick example​

import { useRef } from 'react';
import { useHeadlessGrid, useGridVirtualization } from '@alaarab/ogrid-react';

const ROW_HEIGHT = 32;

// 'all' turns pagination off so every filtered row is available to the window.
const grid = useHeadlessGrid({ columns, data, getRowId, initialPageSize: 'all' });
const containerRef = useRef<HTMLDivElement>(null);
const virt = useGridVirtualization({
rowCount: grid.rows.length,
rowHeight: ROW_HEIGHT,
containerRef,
});

const { startIndex, endIndex, offsetTop, offsetBottom } = virt.rowRange;

<div ref={containerRef} onScroll={virt.onScroll} style={{ height: 400, overflow: 'auto' }}>
<table style={{ borderCollapse: 'separate', borderSpacing: 0 }}>
<tbody>
{offsetTop > 0 && <tr><td colSpan={grid.columns.length} style={{ height: offsetTop, padding: 0 }} /></tr>}
{grid.rows.slice(startIndex, endIndex + 1).map((row) => (
<tr key={grid.getRowId(row)}>
{grid.columns.map((col) => (
<td key={col.columnId} style={{ height: ROW_HEIGHT, boxSizing: 'border-box' }}>
{String(grid.getCellValue(row, col.columnId) ?? '')}
</td>
))}
</tr>
))}
{offsetBottom > 0 && <tr><td colSpan={grid.columns.length} style={{ height: offsetBottom, padding: 0 }} /></tr>}
</tbody>
</table>
</div>

Parameters​

UseGridVirtualizationParams

ParamTypeDefaultDescription
rowCountnumberrequiredTotal rows available to render.
rowHeightnumberrequiredUniform row height in pixels. Must match your CSS exactly.
containerRefRefObject<HTMLElement | null>requiredThe scroll container. Needs a fixed height and overflow: auto.
overscannumber5Extra rows rendered above and below the viewport.
enabledbooleantruefalse returns a pass-through range (render everything).
thresholdnumber100Below this row count virtualization is bypassed to avoid small-dataset artifacts.
columnWidthsnumber[]noneWidths of the unpinned columns. Enables horizontal virtualization.
columnOverscannumber2Extra columns left and right of the viewport.

Returns​

UseGridVirtualizationResult

FieldTypeDescription
totalHeightnumberrowCount * rowHeight. Use it if you prefer a single absolutely-positioned spacer.
rowRange{ startIndex; endIndex; offsetTop; offsetBottom }Inclusive row slice plus spacer heights in pixels.
columnRange{ startIndex; endIndex; leftOffset; rightOffset } | nullColumn slice, or null when columnWidths is omitted or the container has no width yet.
scrollToIndex(index: number, align?: 'start' | 'center' | 'end') => voidScroll the container so a row is in view. Default 'start'.
onScroll() => voidAttach to the container's onScroll.
isActivebooleanenabled && rowCount >= threshold && rowHeight > 0.

Behavior notes​

  • Row height must be exact. Every rendered row has to be rowHeight pixels tall, borders included, or offsets drift as you scroll. For a <table>, border-collapse: separate with box-sizing: border-box on the cells is the predictable setup; the demo uses that.
  • Pair with initialPageSize: 'all' (or pass grid.allFilteredRows) when virtualizing a useHeadlessGrid. grid.rows is otherwise one page, and rowCount must describe the array you slice from.
  • Below threshold nothing is virtualized: rowRange covers every row and isActive is false. The default of 100 means small tables render normally.
  • Scroll updates are coalesced with requestAnimationFrame, and the container size is tracked with ResizeObserver (falling back to the mounted size where it is unavailable).
  • Column virtualization is opt-in and only covers unpinned columns; keep pinned columns outside the columnWidths array and render them yourself.
  • Keep row keys stable (grid.getRowId) so React reuses rows as the window slides instead of remounting them.

See also​