I use react-virtualized Table to render a table with many rows. I don't want my long text to be trimmed due to the fixed column width, so I want to use CellMeasurer to measure the width dynamically.
This is an example using Grid. It works fine.
render() {
const {width} = this.props;
return (
<Grid
className={styles.BodyGrid}
columnCount={1000}
columnWidth={this._cache.columnWidth}
deferredMeasurementCache={this._cache}
height={400}
overscanColumnCount={0}
overscanRowCount={2}
cellRenderer={this._cellRenderer}
rowCount={50}
rowHeight={35}
width={width}
/>
);
}
But neither Table
nor Column
has deferredMeasurementCache
prop. My current code looks like this:
return (
<div>
<AutoSizer disableHeight>
{({width}) => (
<Table
ref="Table"
disableHeader={disableHeader}
headerClassName={styles.headerColumn}
headerHeight={headerHeight}
height={height}
noRowsRenderer={this._noRowsRenderer}
overscanRowCount={overscanRowCount}
rowClassName={this._rowClassName}
rowHeight={useDynamicRowHeight ? this._getRowHeight : rowHeight}
rowGetter={rowGetter}
rowCount={rowCount}
scrollToIndex={scrollToIndex}
sort={this._sort}
sortBy={sortBy}
sortDirection={sortDirection}
width={width}>
<Column
label="Index"
cellDataGetter={({rowData}) => rowData.index}
dataKey="index"
disableSort={!this._isSortEnabled()}
width={60}
/>
<Column .../>
</Table>
)}
</Autosizer>
</div>
);
How can I use Measurer
in Table?
The documented API for react-virtualized does not support using CellMeasurer
in a Table
. That leaves a few options within react-virtualized, including:
Grid
and external column headingsTable
with dependencies on internals not documented in the APIThe 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.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.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:
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:
rowCache
as a deferredMeasurementCache
proprowRenderer
functionrowHeight
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:
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.measuredCellRenderer
, the CellMeasurer
needs to be hard-coded for columnIndex={0}
, even if the measured column is not the first column in the table.if
statement inside the rowHeight
function can be removed since there is no need to transfer heights between two cache's.aMeasuredColumnIndex
can be removed, since it was only referenced in the rowHeight
if
statement.If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With