Virtualization
Virtualization
Virtualization renders only the rows inside the scroll viewport, plus a small buffer, so a table of fifty thousand rows keeps a few dozen elements in the DOM. It is a rendering strategy, not a table feature, and this package does none of it: it has no scroll position, no element heights, and no DOM. This page ports TanStack Table's Virtualization (React) Guide.
TanStack pairs its table with TanStack Virtual. The Elm counterpart is
dominikmayer/elm-virtual-list,
which owns the scroll container, the item offsets, and the visible window.
This page describes the seam between the two: what this package hands a
virtual list, and what you have to set up so the hand-off is correct. The
virtual list package's own documentation covers its side.
Virtualization is not a replacement for server-side paging. The rows still have to be in the browser. If the dataset is too large to load, see Client-Side vs Server-Side.
State
Virtualization owns no state slice, no config option, no column option, no transition, and no query. Everything below uses functions that belong to other guides.
The seam
A virtual list wants three things: a list of items, a stable key per item, and a height per item. The first two come from here.
1. A plain list of rows
rowsInDisplayOrder returns
List (Row row), already filtered, sorted, grouped, and expanded. That is
exactly the input a virtual list indexes into, with no adapter in between:
rowsToVirtualize : Table.State -> List (Table.Row Person)
rowsToVirtualize state =
Table.rowsInDisplayOrder config state (Table.rowsFromList config state people)
2. Every row, not one page
Pagination is always in the pipeline, and its default
page size is 10. A virtual list that only ever sees ten rows is not doing
anything. Turn pagination off for the render path by setting the page size to
unlimitedPageSize, which is
what TanStack writes as Infinity:
everyRow : Table.State
everyRow =
Table.setPageSize Table.unlimitedPageSize Table.initialState
The alternative is to keep pagination out of the render path entirely and stop
the pipeline one stage earlier, at
prePaginationRowModel.
Either way the point is the same: the virtual list has to see every row, or
its scroll height is wrong.
3. A stable id per row
A virtual list keys its items so the browser reuses elements across scroll
frames. Give rows real ids with
withGetRowId, then read them back
with rowId:
config : Table.Config Person
config =
Table.config
[ Table.column "firstName" (.firstName >> Value.String)
|> Table.withHeader "First name"
|> Table.withSize 200
, Table.column "department" (.department >> Value.String)
|> Table.withHeader "Department"
|> Table.withSize 160
]
|> Table.withGetRowId (\person _ _ -> person.id)
rowKeys : Table.State -> List String
rowKeys state =
List.map Table.rowId (rowsToVirtualize state)
Without withGetRowId a row's id is its position in the data, so ids move
when the data reorders, and a virtual list keyed on them reuses the wrong
element.
4. Fixed column widths
The header does not live inside the virtual viewport. It is rendered once, above the scroll container, while the body rows come and go. Nothing lines the two up unless the widths are decided in advance, which means every column needs an explicit width from Column Sizing rather than a width the browser computes from the content of whichever rows happen to be mounted:
columnWidths : Table.State -> List ( String, Float )
columnWidths state =
Table.visibleLeafColumns config state
|> List.map (\column -> ( Table.columnId column, Table.getColumnSize config state column ))
Apply the same number to the header cell and to the body cell, and give the
row a table-layout: fixed container or a flex layout. The same reasoning
applies to row height: a virtual list needs a height per row before it renders
the row, so a fixed row height is the simple case and a measured height is the
complex one.
Virtualized columns
TanStack also virtualizes columns horizontally, over
table.getVisibleLeafColumns(). The counterpart here is
visibleLeafColumns, which is
also a plain list, so the same seam applies: index into it, and take each
column's width from
getColumnSize. Column
virtualization only pays off with dozens of columns; below that, render them
all.
Not covered
TanStack's page is mostly about TanStack Virtual and React: useVirtualizer,
measureElement, absolute positioning inside tbody, the spacer cells for
horizontal virtualization, infinite scrolling with TanStack Query, and the
experimental examples that write DOM styles outside React's render path. All
of that belongs to the virtual list package and to your own view code, not to
this one.
Example
There is no virtualized example, because the virtualizing is done by another package. The nearest one is Pagination, which shows the pagination stage that a virtualized table turns off.