Recyclerlistview: nested recyclerlistview: contextprovider for individual rows being reused

Created on 31 Jul 2018  路  9Comments  路  Source: Flipkart/recyclerlistview

EDIT: reproduced with an expo snack that appears to be related to standard usage, more so than my particular usage: https://snack.expo.io/BJ2CHQRVm

ORIGINAL:
Currently working with a nested recyclerlistview, similar to this project. The difference is that the horizontal and vertical recyclerlistsviews are paginated, meaning their data sources must be updated periodically.

The way I'm currently doing this is to construct the dataproviders and contextproviders for each child row in the parent component. The issue is that the context provider doesn't seem to pass down correctly in props. When recycling large lists, the context provider reuses the context provider from other uniqueIds. This results in a very unfun scrolling experience - as horizontal scroll positions are not preserved when the component is recycled.

Parent dataprovider:

    this.dataProvider = new DataProvider((r1, r2) => {
      return r1.userId !== r2.userId;
    });

Constructing object for each row for the parent dataProvider:

  getDataForRow = uniqueId => {
    let contextProvider;
    if (this._childrenContextProviders[uniqueId]) {
      contextProvider = this._childrenContextProviders[uniqueId];
    } else {
      this._childrenContextProviders[uniqueId] = new ContextHelper(uniqueId);
      contextProvider = this._childrenContextProviders[uniqueId];
    }

    let dataProvider;
    if (this._childrenDataProviders[uniqueId]) {
      dataProvider = this._childrenDataProviders[uniqueId];
    } else {
      this._childrenDataProviders[uniqueId] = new DataProvider((r1, r2) => {
        return r1 !== r2;
      });
      dataProvider = this._childrenDataProviders[uniqueId];
    }

    return {
      uniqueId: uniqueId,
      contextProvider,
      dataProvider,
    };
  };

Then my renderRow() function looks as follows:

  _rowRenderer = (type, data) => {
    if (data.userId === 'header') {
      return this.renderListHeader();
    }

    return (
      <CellComponent
        uniqueId={data.uniqueId}
        contextProvider={data.contextProvider}
        dataProvider={data.dataProvider}
      />
    );
  };

Most helpful comment

It is working fine @alexpareto. Problem is with your log statement

   this.list.scrollToOffset(
          this.props.getCachePosition(this.props.uniqueId),
          0,
          false
        );
  console.log(
          'scrolled from ',
          this.props.getCachePosition(prevProps.uniqueId),
          'to',
          this.list.getCurrentScrollOffset(),
          'supposed to scroll to',
          this.props.getCachePosition(this.props.uniqueId)
        );

Given scrollToOffset is asynchronous this.list.getCurrentScrollOffset() wouldn't have updated by the time you are logging it. Same is true with DataProvider change.

If I wrap the log part in double rAF it works fine. Check: https://snack.expo.io/B1B0YU-HX

All 9 comments

here is an expo snack that reproduces the issue (at least on iOS), using the same code from the context provider example project but with 100 children rows in order to force the recycling: https://snack.expo.io/BJ2CHQRVm

To reproduce, scroll one row to the right. Now scroll through the list quickly. Notice how rows which you haven't scrolled yet, are recycled to use the wrong context provider.

ContextProvider only works while mounting. During recycling nothing will mount again. If you want to recycle RecyclerListView instances you'll have to cache scroll positions manually and apply them again in componentDidUpdate inside your component. Let me know if this helps.

Hey, that makes sense with regard to the context provider. Decided to go ahead on cache in componentDidUpdate and then use scrollTo to update the list. Still running into some bugs though. While the cacheing mechanism works fine (getting the right location). The scrollTo() method tends to just not run.

e.g. standard console output:
scrolled from 341 to 341 supposed to scroll to 0

here's an expo snack reproducing (scroll 3 or 4 rows in the top to various spots, then scroll through the list and note the blank spaces).
https://snack.expo.io/S1VcpukH7

Any ideas? Do refs get messed up in the recycle process?

Perhaps it's something with _pendingScrollToOffset? Does the recycle process set that - and then void any other scrollTos?

It appears something like that is the issue...delaying the scrollTo 100ms solves the issue, but feels like an ugly hack.

        setTimeout(() => {
          this.list.scrollToOffset(
            this.props.getCachePosition(this.props.userId),
            0,
            false,
          );
        }, 100);

It is working fine @alexpareto. Problem is with your log statement

   this.list.scrollToOffset(
          this.props.getCachePosition(this.props.uniqueId),
          0,
          false
        );
  console.log(
          'scrolled from ',
          this.props.getCachePosition(prevProps.uniqueId),
          'to',
          this.list.getCurrentScrollOffset(),
          'supposed to scroll to',
          this.props.getCachePosition(this.props.uniqueId)
        );

Given scrollToOffset is asynchronous this.list.getCurrentScrollOffset() wouldn't have updated by the time you are logging it. Same is true with DataProvider change.

If I wrap the log part in double rAF it works fine. Check: https://snack.expo.io/B1B0YU-HX

Ah yes, you're right! good catch. Thanks for the help + the component.

Hello i wanted to ask something , can i use the ContextProvider in the web as i tried many times and it didn't work it seems i am missing something !

@naqvitalha I know this is a fairly old issue, but I'm still having problems with scrollToOffset.

Using https://snack.expo.io/B1B0YU-HX it works in all situations except when one of the horizontal lists is manually scrolled to the very end. In that situation, the scrollToOffset function does not fire at all and the new lists using the recycled component are scrolled to the end as well.

Interestingly, this is device-dependent, and adding extra print statements at the top causes the issue to go away. It seems to be very dependent on timing - some devices/simulators have the issue, others don't.

As a workaround, going back to the hack that @alexpareto used with an explicit timeout fixes the issue entirely.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

sm2017 picture sm2017  路  5Comments

mmamoyco picture mmamoyco  路  7Comments

ajinkyadesai11-git picture ajinkyadesai11-git  路  4Comments

jasonmerino picture jasonmerino  路  6Comments

LukaszK88 picture LukaszK88  路  6Comments