Search code examples
react-virtualized

Using CellMeasurer with MultiGrid


I may be trying to do something that isn't supported, but I am trying to use react-virtualized's CellMeasurer with MultiGrid. I also am using a ScrollSync to detect when the user has scrolled all the way to the right and show/hide an indicator.

One caveat is that I have a tab control that manipulates what data (both rows and columns). I have a flag set in my redux store when the data has changed, and am using that to remeasure my cells.

It is working pretty closely to what I would expect. The first time I go to a new tab, the cells are all measured correctly with two exceptions.

1) The first column (1 fixed column) remeasures, but the width of the top left, and bottom left grids does not update. This leaves a gap between the new measurement and the default size. Once I scroll, this fixes itself - pretty sure because I have the ScrollSync.

Before Scroll enter image description here After Scroll enter image description here

2) Column index 1 never gets smaller than the default width. This is the first non-fixed column. enter image description here enter image description here Works with larger Content: enter image description here

Then, the main issue is when I return to tabs that have already been shown before. When this happens, the measurements from columns that existed in the previous tab carry over even though my flag for new data is still triggering a remeasure. I think I need to do something with clearing the cache, but my attempts so far have resulted in all columns going to the default width. Is there a certain sequence of CellMeasurerCache.clearAll, MultiGrid.measureAllCells and MultiGrid.recomputeGridSize that will work properly for me here?

Render

  render() {
    const { tableName, ui } = this.props;
    const dataSet = this.getFinalData();
    console.log('rendering');

    return (
      <ScrollSync>
        {({
          // clientHeight,
          // scrollHeight,
          // scrollTop,
          clientWidth, // width of the grid
          scrollWidth, // width of the entire page
          scrollLeft, // how far the user has scrolled
          onScroll,
        }) => {
          // if we have new daya, default yo scrolled left
          const newData = Ui.getTableNewData(ui, tableName);

          const scrolledAllRight = !newData &&
              (scrollLeft + clientWidth >= scrollWidth);
          const scrolledAllLeft = newData || scrollLeft === 0;

          return (
            <AutoSizer>
              {({ width, height }) => {
                const boxShadow = scrolledAllLeft ? false :
                    '1px -3px 3px #a2a2a2';
                return (
                  <div className="grid-container">
                    <MultiGrid
                      cellRenderer={this.cellRenderer}
                      columnWidth={this.getColumnWidth}
                      columnCount={this.getColumnCount()}
                      fixedColumnCount={1}
                      height={height}
                      rowHeight={this.getRowHeight}
                      rowCount={dataSet.length}
                      fixedRowCount={1}
                      deferredMeasurementCache={this.cellSizeCache}
                      noRowsRenderer={DataGrid.emptyRenderer}
                      width={width}
                      className={classNames('data-grid', {
                        'scrolled-left': scrolledAllLeft,
                      })}
                      onScroll={onScroll}
                      styleBottomLeftGrid={{ boxShadow }}
                      ref={(grid) => {
                        this.mainGrid = grid;
                      }}
                    />
                    <div
                      className={classNames('scroll-x-indicator', {
                        faded: scrolledAllRight,
                      })}
                    >
                      <i className="fa fa-fw fa-angle-double-right" />
                    </div>
                  </div>
                );
              }}
            </AutoSizer>
          );
        }}
      </ScrollSync>
    );
  }

Cell Renderer

  cellRenderer({ columnIndex, rowIndex, style, parent }) {
    const data = this.getFinalData(rowIndex);
    const column = this.getColumn(columnIndex);

    return (
      <CellMeasurer
        cache={this.cellSizeCache}
        columnIndex={columnIndex}
        key={`${columnIndex},${rowIndex}`}
        parent={parent}
        rowIndex={rowIndex}
        ref={(cellMeasurer) => {
          this.cellMeasurer = cellMeasurer;
        }}
      >
        <div
          style={{
            ...style,
            maxWidth: 500,
          }}
          className={classNames({
            'grid-header-cell': rowIndex === 0,
            'grid-cell': rowIndex > 0,
            'grid-row-even': rowIndex % 2 === 0,
            'first-col': columnIndex === 0,
            'last-col': columnIndex === this.getColumnCount(),
          })}
        >
          <div className="grid-cell-data">
            {data[column.key]}
          </div>
        </div>
      </CellMeasurer>
    );
  }

Component Lifecycle

  constructor() {
    super();

    this.cellSizeCache = new CellMeasurerCache({
      defaultWidth: 300,
    });

    // used to update the sizing on command
    this.cellMeasurer = null;
    this.mainGrid = null;

    // this binding for event methods
    this.sort = this.sort.bind(this);
    this.cellRenderer = this.cellRenderer.bind(this);
    this.getColumnWidth = this.getColumnWidth.bind(this);
    this.getRowHeight = this.getRowHeight.bind(this);
  }

  componentDidMount() {
    this.componentDidUpdate();

    setTimeout(() => {
      this.mainGrid.recomputeGridSize();
      setTimeout(() => {
        this.mainGrid.measureAllCells();
      }, 1);
    }, 1);
  }

  componentDidUpdate() {
    const { tableName, ui } = this.props;

    // if we did have new data, it is now complete
    if (Ui.getTableNewData(ui, tableName)) {
      console.log('clearing');
      setTimeout(() => {
        this.mainGrid.measureAllCells();
        setTimeout(() => {
          this.mainGrid.recomputeGridSize();
        }, 1);
      }, 1);
      this.props.setTableNewData(tableName, false);
    }
  }

EDIT Here is a plunker. This example shows most of what I was explaining. It is also is giving more height to the rows than expected (can't tell what is different than my other implementation)


Solution

  • First suggestion: Don't use ScrollSync. Just use the onScroll property of MultiGrid directly. I believe ScrollSync is overkill for this case.

    Second suggestion: If possible, avoid measuring both width and height with CellMeasurer as that will require the entire Grid to be measured greedily in order to calculate the max cells in each column+row. There's a dev warning being logged in your Plnkr about this but it's buried by the other logging:

    CellMeasurerCache should only measure a cell's width or height. You have configured CellMeasurerCache to measure both. This will result in poor performance.

    Unfortunately, to address the meat of your question- I believe you've uncovered a couple of flaws with the interaction between CellMeasurer and MultiGrid.

    Edit These flaws have been addressed with the 9.2.3 release. Please upgrade. :)

    You can see a demo of CellMeasurer + MultiGrid here and the source code can be seen here.