Skip to content

Architecture useGridVisibleRows

github-actions[bot] edited this page Oct 1, 2026 · 1 revision

📝 Generated from docs/architecture/use-grid-visible-rows.md. Edit it there; changes made in the wiki are overwritten.

useGridVisibleRows

Internal hook. Merges the pinned top rows, the virtual center window, and the pinned bottom rows into a single { row, rowIndex }[] array — exactly what the viewport JSX iterates to render <Row> components.

File: lib/hooks/core/useGridVisibleRows.ts


Purpose

The render window is the intersection of what the row pipeline produced and what the virtualization engine says is visible. Before this hook, ~40 lines of useMemo logic lived inline in DataGrid.tsx. Extracting it makes the merge strategy explicit and independently testable.


Parameters

interface UseGridVisibleRowsParams<R extends GridRowModel> {
    renderContext: {
        firstRowIndex: number;  // from useGridVirtualization
        lastRowIndex: number;
    };
    pinnedTopRows: R[];             // from useGridRowPipeline
    pinnedBottomRows: R[];          // from useGridRowPipeline
    paginatedUnpinnedRows: R[];     // from useGridRowPipeline
    sortedUnpinnedRows: R[];        // from useGridRowPipeline
    pagination: boolean;
}

Returns

GridVisibleRow<R>[]
// where GridVisibleRow<R> = { row: R; rowIndex: number }

rowIndex is the position in the renderable rows (top-pinned + the current page's centre rows + bottom-pinned) — it becomes the row's data-rowindex, the rowIndex in onRowClick / getDetailPanelContent params and the zebra-stripe class. aria-rowindex is computed separately (lib/utils/aria, getAriaRowLayout): it adds the header rows and, for centre rows, the rows on earlier pages, and puts bottom-pinned rows after every page. GridPinnedRows receives the bottom rows' offset (topPinned + centerCount) so they no longer restart at 0.


Merge strategy

topPinned:  rows 0 .. pinnedTopRows.length - 1              (always fully rendered)
center:     rows [firstRowIndex - topPinnedCount, lastRowIndex - topPinnedCount]
            (clipped to the virtual window)
bottomPinned: rows (topPinned + centerCount) .. end          (always fully rendered)

Pinned rows are NOT subject to virtualization — they are always in the DOM regardless of scroll position.


Deduplication guard

After merging, the hook filters out any row whose id has already been seen:

const seenIds = new Set<GridRowId>();
return combined.filter(item => {
    if (seenIds.has(item.row.id)) return false;
    seenIds.add(item.row.id);
    return true;
});

Why: If a user passes the same row in both rows and pinnedRows, it would appear in both pinnedTopRows and paginatedUnpinnedRows. Two <Row key={id}> elements with the same key cause React reconciliation bugs. The guard ensures each ID appears at most once — the pinned version takes precedence since it comes first in the merged array.

Entries from the pinned sections carry pinned: 'top' | 'bottom'. GridVirtualRows renders only the untagged (center) entries, in one linear pass; it does not de-duplicate again and does not consult the raw pinnedRows prop, so under tree data / row grouping (where nothing is pinned) every row still renders in its place.


Relationship to DataGrid

DataGrid.tsx calls this hook after useGridVirtualization produces the render context and after useGridRowPipeline produces the pinned/paginated row groups. The returned visibleRows array is iterated directly in the viewport JSX.


🔗 Related

Clone this wiki locally