After some trial and error I found a good way to do this is to make the rowRenderer
function decide which way to render the row. You can do this by checking the _rowHeightCache
property in the CellMeasurerCache
instance use use in your list. Note that the keys of _rowHeightCache
take the form: "index
-0" where index
is the row's index.
Here is how you can set up the row renderer:
TextRowRenderer(
{
key, // Unique key within array of rows
index, // Index of row within collection
// isScrolling, // The List is currently being scrolled
style, // Style object to be applied to row (to position it)
parent, // reference to List
},
) {
const { textArray } = this.props;
// get the cached row height for this row
const rowCache = this.listCache._rowHeightCache[`${index}-0`];
// if it has been cached, render it using the normal, more expensive
// version of the component, without bothering to wrap it in a
// CellMeasurer (it's already been measured!)
// Note that listCache.defaultHeight has been set to 0, to make the
// the comparison easy
if (rowCache !== null && rowCache !== undefined && rowCache !== 0) {
return (
<TextChunk
key={key}
chunk={textArray[index]}
index={index}
style={style}
/>
);
}
// If the row height has not been cached (meaning it has not been
// measured, return the text chunk component, but this time:
// a) it's wrapped in CellMeasurer, which is configured with the
// the cache, and
// b) it receives the prop textOnly={true}, which it tells it to
// to skip rendering the markup
return (
<CellMeasurer
cache={this.listCache}
columnIndex={0}
key={key}
parent={parent}
rowIndex={index}
>
{() => {
return (
<TextChunk
key={key}
chunk={textArray[index]}
index={index}
style={style}
textOnly
/>
);
}}
</CellMeasurer>
);
}
This is then passed to the List
component in the ordinary way:
<List
height={pageHeight}
width={pageWidth}
rowCount={textArray.length}
rowHeight={this.listCache.rowHeight}
rowRenderer={this.TextRowRenderer}
deferredMeasurementCache={this.listCache}
style={{ outline: 'none' }}
ref={(r) => { this.listRef = r; }}
/>
By including a reference callback, we can now access the
measureAllRows
method, which we can use to force the List
to render all rows in advance to get the heights. This will
ensure that the scroll bar functions properly, but even with
the textOnly
flag can take a while longer. In my case, I believe
it is worth the wait.
Here is how you can call measureAllRows
:
componentDidUpdate() {
const {
textArray,
} = this.props;
// We will only run measureAllRows if they have not been
// measured yet. An easy way to check is to see if the
// last row is measured
const lastRowCache = this
.listCache
._rowHeightCache[`${textArray.length - 1}-0`];
if (this.listRef
|| lastRowCache === null
|| lastRowCache === undefined
|| lastRowCache === 0) {
try {
this.listRef.measureAllRows();
} catch {
console.log('failed to measure all rows');
}
}
}
The try-catch block is needed because if it tries to measure
before the List
is constructed it will throw an index-out-of
-bounds error.
FINAL THOUGHTS
Another possible way to do this would be to have the actual List
component be conditional on cached measurements rather than the
rowRenderer
function. You could render an array of text as a a
regular list, wrapping each row in a <CellMeasurer>
. When the
cache is filled, you would then render a virtual List
configured
using the prefilled cache. This would obviate the need to call
measureAllRows
in componentDidUpdate
or a useEffect
callback.