Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use CellMeasurer in react-virtualized Table?

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?

like image 358
Yixing Liu Avatar asked Aug 13 '18 16:08

Yixing Liu


1 Answers

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.
like image 195
sudr minz Avatar answered Sep 20 '22 08:09

sudr minz