1
votes

I'm developing a data grid component based on react-virtualized. It's supposed to have a fixed header with resizable columns. I want the header to change its height according to the header cells content. I'm using CellMeasurer to calculate cells height and update height of the header.

The problem is that cell sizes are calculated after the cells are rendered (afaik), so I have to call forceUpdate inside of the header's render if height has been changed.

here is how render looks like (complete example is here):

render() {
    const height = this._cache.rowHeight({ index: 0 });
    console.log('render', height, this.props.sizes);
    setTimeout(() => {
      if (height !== this._cache.rowHeight({ index: 0 })) {
        console.log('forceUpdate', this._cache.rowHeight({ index: 0 }))
        this.forceUpdate()
      }
    }, 0)

    return (
      <Grid 
        ref={ref => this._grid = ref}
        deferredMeasurementCache={this._cache}
        width={1500}
        height={height}
        columnCount={5}
        columnWidth={this.columnWidth}
        rowCount={1}
        rowHeight={this._cache.rowHeight}
        cellRenderer={this.renderCell}
      />
    );
  }

so the question is how do I avoid forceUpdate? Is there a cleaner way to implement grid header with dynamic height using react-virtualized?

2

2 Answers

2
votes

Setting timeouts in render is probably not a good idea.

If the height of a cell changes (eg the value returned from rowHeight) then CellMeasurer will automatically notify the parent Grid. In that case, Grid will automatically clear its position cache and re-render.

What you're doing differently in this example is setting the outer height of the Grid to be equal to the cell-measured height. This isn't a common use-case, but you could support it without the timer-hack like so:

class XHeader extends React.Component {
  _cache = new CellMeasurerCache({
    defaultHeight: 20,
    fixedWidth: true,
  });
  state = {
    headerHeight: 20,
  };
  renderCell = props => {
    return (
      <HeaderCell 
        width={this.props.sizes[props.columnIndex]}
        {...props}
        cache={this._cache}
        onMouseDown={this.props.onMouseDown}
      />
    );
  }
  columnWidth = ({ index }) => {
    return this.props.sizes[index];
  }
  render() {
    return (
      <Grid 
        ref={ref => this._grid = ref}
        deferredMeasurementCache={this._cache}
        width={1500}
        height={this.state.headerHeight}
        columnCount={5}
        columnWidth={this.columnWidth}
        onSectionRendered={this._onSectionRendered}
        rowCount={1}
        rowHeight={this._cache.rowHeight}
        cellRenderer={this.renderCell}
      />
    );
  }

  _onSectionRendered = ({ rowOverscanStartIndex }) => {
    if (rowOverscanStartIndex === 0) {
      const height = this._cache.rowHeight({ index: 0 });

      if (height !== this.state.headerHeight) {
        this.setState({
          headerHeight: height,
        });
      }
    }
  }
}
1
votes

I feel like I have to post it as an answer.

Brian pointed to the right direction, thanks Brian for your amazing work and support.

That onSectionRendered handler is important, because it lets us update header's height after the first render:

_onSectionRendered = ({ rowOverscanStartIndex }) => {
    if (rowOverscanStartIndex === 0) {
      const height = this._cache.rowHeight({ index: 0 });

      if (height !== this.state.headerHeight) {
        this.setState({
          headerHeight: height,
        });
      }
    }
  }

componentWillReceiveProps from the original codesandbox is important as well -- it what makes component react on the columns resizing:

componentWillReceiveProps(nextProps) {
    if (nextProps.sizes !== this.props.sizes) {
      this._cache.clearAll();
      console.log('recomputeGridSize');
      this._grid.recomputeGridSize();
      console.log('===', this._cache.rowHeight({ index: 0 }))
    }
  }

What was missing is componentDidUpdate:

componentDidUpdate() {
    console.log('componentDidUpdate',
      this._cache.rowHeight({ index: 0 }));
    const height = this._cache.rowHeight({ index: 0 });

    if (height !== this.state.headerHeight) {
      this.setState({
        headerHeight: height,
      });
    }
  }

-- this is the method where we can check whether the row's height differs from the header's height and update it if necessary. componentDidUpdate is not called after initial render, that's why onSectionRendered is helpful.

Though it doesn't avoid an extra render, it's much cleaner than timeout hack. Complete example is here: https://codesandbox.io/s/kor4vv6jqv.