4

I have a react component that displays a one-dimensional list of items in a grid pattern. Every item has the same width and height, and they are located within three columns. There can be a lot of these, so I decided to give react-virtualized a whirl, using the Masonry component, which seems to have been written for precisely this use case. It works well, but for one caveat: If the items change their height, I cannot get Masonry to notice.

Here's a reduced example:

constructor(props) {
    [...]
    this.cellMeasurerCache = new CellMeasurerCache({
        defaultHeight: this.state.height,
        fixedWidth: true,
    });

    this.cellPositioner = createMasonryCellPositioner({
        cellMeasurerCache: this.cellMeasurerCache,
        columnCount: 3,
        columnWidth: 250,
        spacer: 20,
    });
}

[...]

cellRenderer({ index, key, parent, style }) {
    return (
        <CellMeasurer
            cache={this.cellMeasurerCache}
            index={index}
            key={key}
            parent={parent}
        >
            <div style={style}>
                [...]
            </div>
        </CellMeasurer>
    );
}

resetMasonry() {
    this.cellMeasurerCache.clearAll();

    this.cellPositioner.reset({
        cache: this.cellMeasurerCache,
        columnCount: 3,
        columnWidth: 250,
        spacer: 20,
    });

    this.masonryRef.recomputeCellPositions();
}

render() {
    return (
        <AutoSizer>
            {({ height, width }) =>
                <Masonry
                    cellCount={this.state.list.length}
                    cellMeasurerCache={this.cellMeasurerCache}
                    cellPositioner={this.cellPositioner}
                    cellRenderer={this.cellRenderer}
                    height={height}
                    ref={this.setMasonryRef}
                    width={width}
                />
            }
        </AutoSizer>
    );
}

resetMasonry is called when the component's height state has changed. I have cargo-culted the current code from several stackoverflow answers and other resources, even went over the source code, but nothing works. I've noticed that I'm not telling the cellMeasurerCache or anything else about the height change, so I really shouldn't expecting it to work, but there seems to be no way to get that information in there, not even by instantiating a new CellMeasurerCache.

Incidentally, if I change the columnWidth within cellPositioner.reset, the Masonry component updates accordingly, sure enough.

Does anybody know what I'm missing to get it working for a height change? Thanks!

Martin Denk
  • 554
  • 3
  • 14

1 Answers1

9

If your Masonry cell's sizes change, you need to clear the cached sizes in your CellMeasurerCache and then make sure the cellPositioner is aware of the new sizes also. For example:

// Clear cached measurements since sizes have changed:
cellMeasurerCache.clearAll()

// Let your component know it needs to re-render
this.setState({...}, () => {
  // Assuming you're using the default createCellPositioner()
  // Let it know of any values that may have changed:
  cellPositioner.reset({
    columnCount,
    columnWidth,
    spacer
  })

  // Tell Masonry to discard any cached position data:
  masonryRef.clearCellPositions()
})

You can see a demo of a Masonry component with changing column heights on the RV site just by changing the column width. The source code for that demo is available in the GitHub repo also.

From what you're describing though, I wouldn't suggest using Masonry. I would actually just suggest using a List in a similar way to how I did in this example: http://plnkr.co/edit/zjCwNeRZ7XtmFp1PDBsc?p=preview

The key bit is to calculate the number of items per row dynamically based on the available width, and then you give List an adjusted rowCount:

  const itemsPerRow = Math.floor(width / ITEM_SIZE);
  const rowCount = Math.ceil(ITEMS_COUNT / itemsPerRow);
bvaughn
  • 13,300
  • 45
  • 46
  • Thanks a lot for your answer! Using a `List` turned out to be the right choice (I aleady knew how to use it because I had done so in another component that displayed a long list of full-width items). The number of columns in my use case never changes, so it was easy enough to adapt. Now I'm wondering what use cases the `Masonry` component is for, if not this? Also, it turned out what I was missing was to use `clearCellPositions` rather than `recomputeCellPositions`. Doing that caused the component to notice the change in height, but it only updated a number of its items, not all of them... – Martin Denk Jun 14 '17 at 10:16
  • In any case, using a simple `List` works very well, so thanks again! – Martin Denk Jun 14 '17 at 10:18
  • 1
    `Masonry` is intended for Pinterest layouts. In fact I built the component for Pinterest to use specifically, after talking with one of their engineers about it. (Not sure if they chose to use it or not.) Since the heights of your items are all the same, `List` is easier to use. :) – bvaughn Jun 14 '17 at 15:10