react-virtualized: scrollToRow on List with dynamic heights and CellMeasurer doesn't always scroll to the proper location

On a List with many items, all of dynamic height that are measured using a CellMeasurer, sometimes, jumping to a smaller row index n from larger row m index fails. This more frequently happens when m is much larger than n.

Here’s a plunkr to reproduce this behavior: https://plnkr.co/edit/B1463DyuByuIlmWTynRn

Steps:

  1. Enter a number between 600-800 in the box, and hit “Jump to row…”
  2. Enter a number between 200-400 in the box, and hit “Jump to row…”. Check whether the row is visible.
  3. Repeat steps 1 and 2, using a different number in the ranges each time, until you notice that the row in step 2 isn’t visible after the jump.

It usually takes anywhere from 1-5 repetitions for it to first occur. I’ve also noticed the error occurs with decreasing frequency the more jumps are done.

About this issue

  • Original URL
  • State: open
  • Created 6 years ago
  • Reactions: 23
  • Comments: 16 (1 by maintainers)

Most upvoted comments

I have solution

/**
 * Scroll to bottom of List/Grid
 */
const step = () => {
  const element = // .ReactVirtualized__Grid or List element
  const maxScrollTop = element.scrollHeight - element.clientHeight;

  if (maxScrollTop > element.scrollTop) {
     listRef.scrollToPosition(maxScrollTop); // Ref to <List /> component instance (or Grid)
     requestAnimationFrame(step);
  }
};

step();

You should do scroll until row index is >= startIndex and <= stopIndex scrollTop can be found by List#getOffsetForRow

I have the same issue, my workaround is to call scrollToRow again in the setState callback, like:

this.listNode.scrollToRow(idx);
this.setState({ scrollingToRow: idx }, () => {
    this.listNode.scrollToRow(idx);
});

Are there any other workarounds or solutions ?

BTW @oliverodaa, calling this.ref.measureAllRows() not solving for me

I was having same problem in List solved it by using hack of setting an estimatedRowSize parameter to average row size value. StackOverflow Answer

A final comment:

You could actually get measureAllCells() to give correct truly measured data if you set defaultHeight: 0 within the cache - after which you would not need the double scrollToRow.

Of course, depending on your product, this is either not feasible, or totally feasible. Tread lightly.

Okay I have revisited this after a year to try to get a better solution. Here’s what I came up with:

  • ScrollToRow should be a promise that resolves only after the row is rendered.
  • As @ryanwilliamquinn said, calling twice is really the only workable solution I found.
  • It’s not necessary to call twice if you have already measured the height of the row you are scrolling to

So we basically just try twice and then resolve the promise after the second try.

const getElementById = id => listRef.current.Grid._scrollingContainer.querySelector(`#${id}`);

let promisedRow = null;

const scrollToRow = index =>
  new Promise(async resolve => {
    // If the elem is already on the page, do nothing
    if (getElementById(`row-${index}`)) return resolve();

    // If onRowsRendered is not called enough times to resolve the promise, this resolves it
    setTimeout(() => promisedRow && resolve(), 250);

    // If we've seen the row before, scroll once. If we haven't, scroll twice.
    // See this issue for explanation: https://github.com/bvaughn/react-virtualized/issues/995
    promisedRow = { index, resolve, remainingTries: cache._rowHeightCache[`${index}-0`] ? 0 : 1 };

    listRef.current.scrollToRow(index);
  });

// This function is called in onRowsRendered as a kind of "callback" after the scroll event occurs
const scrollToRowCallback = () => {
  if (!promisedRow) return;

  const { index, resolve, remainingTries } = promisedRow;
  if (remainingTries) {
    promisedRow.remainingTries--;
    listRef.current.scrollToRow(index);
  } else {
    promisedRow = null;
    resolve();
  }
};

<List {...otherProps} onRowsRendered={scrollToRowCallback} />