The documented API for react-virtualized does not support using CellMeasurer
in a Table
. That leaves a few options within react-virtualized, including:
- implement using
Grid
and external column headings
- implement using
Table
with dependencies on internals not documented in the API
The following outlines a solution to the latter approach that works with react-virtualized v9.21.1 (the latest version as of July, 2019). Of course such an approach risks that changes to internals in future releases of react-virtualized will break something.
There are several issues to deal with, including:
Table
uses a Grid
internally to provide virtualized scrolling, but doesn't expose it in the API everywhere it is needed.
- The
Grid
only has one column, which contains all of the Column
cells in a row, but the Grid
is passed as the parent for rendering Column
cells. As a result, one Grid
cell can be associated with many Column
cells, a situation that Grid
and CellMeasurer
do not support.
- Use of
CellMeasurer
in a Grid
depends on the Grid
directly managing all of the cells in a row, without an intervening rowRenderer
, while Table
has its own row rendering logic.
[In the code examples that follow, some data elements and functions are shown with module-level declarations. In practice they could instead be defined as members of a component which contains the Table
or in some cases, perhaps passed as props to a component which contains the Table
.]
The following solution resolves these issues in a general case of:
- static table data
- static row, column, and cell formatting
- the heights of cells within a column are variable across rows
- multiple columns can have such variable-height cells
For that case, two instances of CellMeasurerCache are used. cellCache
is for the height of individual Column
cells. rowCache
is for the height of rows.
const cellCache = new CellMeasurerCache({
fixedWidth: true,
defaultHeight: 20, // keep this <= any actual row height
minHeight: 10, // keep this <= any actual row height
});
const rowCache = new CellMeasurerCache({
fixedWidth: true,
defaultHeight: 37, // tune as estimate for unmeasured rows
minHeight: 10, // keep this <= any actual row height
});
For the Table
component:
- add
rowCache
as a deferredMeasurementCache
prop
- add a
rowRenderer
function
- add a
rowHeight
function
<Table
...
deferredMeasurementCache={rowCache}
rowRenderer={rowRenderer}
rowHeight={rowHeight}
>
The functions will be shown later. Table
won't do anything with the deferredMeasurementCache
except pass it along as a prop to the Table
's Grid
.
For every Column
that needs to be measured, add a cellRenderer
function. In many simpler cases, the same function can be used for all measured columns:
<Column
...
cellRenderer={measuredCellRenderer}
/>
To help coordinate use of the two caches, three additional data items are needed:
const aMeasuredColumnIndex = 2; // any measured column index will do
let rowParent = null; // let a cellRenderer supply a usable value
const cellParent = { // an intermediary between measured row cells
// and their true containing Grid
invalidateCellSizeAfterRender: ({rowIndex}) => {
if (rowParent &&
typeof rowParent.invalidateCellSizeAfterRender == 'function') {
rowParent.invalidateCellSizeAfterRender({columnIndex: 0, rowIndex});
}
},
}
rowParent
is used to expose the Table's Grid to the rowRenderer. cellParent
serves as an intermediary between the two caches and between a row, its Column
cells, and the Table
's Grid
.
Next are the three functions that were previously mentioned:
function measuredCellRenderer({rowIndex, columnIndex, parent, cellData}) {
rowParent = parent; // parent is the Table's grid,
// save it for use by rowRenderer
return (
<CellMeasurer
cache={cellCache}
columnIndex={columnIndex}
parent={cellParent}
rowIndex={rowIndex}
>
<div>{cellData}</div>
</CellMeasurer>
);
// Note: cellData is wrapped in a <div> to facilitate height
// styling, for example adding padding to the <div>, because CellMeasurer
// measures the height of the content box.
}
function rowRenderer(params) {
return (
<CellMeasurer
cache={rowCache}
columnIndex={0}
key={params.key}
parent={rowParent}
rowIndex={params.rowIndex}
>
{Table.defaultProps.rowRenderer(params)}
</CellMeasurer>
);
}
function rowHeight({index}) {
let cellCacheRowHeight = cellCache.rowHeight({index});
if (cellCache.has(index, aMeasuredColumnIndex)) {
rowCache.set(index, 0, 20, cellCacheRowHeight);
// the 20 above is a somewhat arbitrary number for width,
// which is not relevant
}
return cellCacheRowHeight;
}
Note that there are two different uses of CellMeasurer
. One is inside the measuredCellRenderer
function and uses cellCache
and cellParent
. The other is inside the rowRenderer
function and uses rowCache
and rowParent
.
Also, the rowHeight
function doesn't just report a row's height. It is also responsible for transferring the row's rowHeight
in cellCache
to the row's cell height for the first and only column in rowCache
.
This solution can be simplified somewhat when the table has only one measured column. Only one CellMeasurerCache
is needed. A single cache can fulfill the
role of both cellCache
and rowCache
. As a result:
- There is no need for
cellParent
; it can be removed. References to cellParent
can be replaced by references to rowParent
, or in the case of the measuredCellRenderer
function, the CellMeasurer
parent
prop can be set directly to the parent
function argument.
- Inside
measuredCellRenderer
, the CellMeasurer
needs to be hard-coded for columnIndex={0}
, even if the measured column is not the first column in the table.
- The
if
statement inside the rowHeight
function can be removed since there is no need to transfer heights between two cache's.
- The
aMeasuredColumnIndex
can be removed, since it was only referenced in the rowHeight
if
statement.