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.
2) Column index 1 never gets smaller than the default width. This is the first non-fixed column.
Works with larger Content:
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)
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.