React-native: FlatList onEndReached Called On Load [iOS] (with detailed findings)

Created on 22 Sep 2017  ·  59Comments  ·  Source: facebook/react-native

Is this a bug report?

Yes

Have you read the Contributing Guidelines?

Yes

Environment

Environment:
OS: macOS Sierra 10.12.6
Node: 6.10.3
Yarn: 1.0.2
npm: 3.10.10
Watchman: 4.7.0
Xcode: Xcode 8.3.3 Build version 8E3004b
Android Studio: Not Found

Packages: (wanted => installed)
react: 16.0.0-alpha.12 => 16.0.0-alpha.12
react-native: 0.48.3 => 0.48.3

Steps to Reproduce

  1. Create minimal app with a FlatList component that loads 20 items (with a height that is roughly double the height of the screen size)
  2. Add onEndReached prop with a handler function that adds more data to the FlatList
  3. Add onEndReachedThreshold prop with a value of 0.5
  4. Load the app with the iOS simulator or an actual device
<FlatList
  data={this.state.data}
  keyExtractor={(item, index) => index}
  renderItem={this._renderItem}
  onEndReached={this.onEndReached}
  onEndReachedThreshold={0.5} />

Expected Behavior

onEndReached should not be called until the user scrolls down on the vertical FlatList.

Actual Behavior

onEndReached is called as the FlatList is loaded without any interaction from the end user. There seems to be some kind of race condition, as it only happens 50% of the time I load the screen. It is definitely reproducible with a couple refreshes.

Reproducible Demo

Preferred way to reproduce is to download my minimal repo from https://github.com/zachrnolan/react-native-flatlist-vertical and yarn install && react-native run-ios

I also created a snack here: https://snack.expo.io/HkkO0RGob. Although it seems to be a little buggy with displaying the console logs.

Detailed Findings

Since starting with React Native about a year and a half ago, I've always dealt with some inconsistencies or bugs with lists and infinite scroll / paging. I've outlined my findings with FlatList to hopefully allow other people to better understand the landscape. This could be too much detail for this particular bug report, but I think it's helpful.

Platform | Number of Items | onEndReachedThreshold
--- | --- | ---
iOS | 5 (roughly half the screen height) | 0

Results:
onEndReached is called twice on load of the FlatList

Notes:
I'm guessing that onEndReached is called after the load of FlatList since the bottom of the FlatList is in view. It seems like the user should have to interact with the FlatList before onEndReached is called. Either way, shouldn't be called twice.


Platform | Number of Items | onEndReachedThreshold
--- | --- | ---
iOS | 10 (roughly the same height as the screen) | 0

Results:
onEndReached is called once on load of the FlatList

Notes:


Platform | Number of Items | onEndReachedThreshold
--- | --- | ---
iOS | 10 (roughly the same height as the screen) | 0.5 OR 1

Results:
Same as above


Platform | Number of Items | onEndReachedThreshold
--- | --- | ---
iOS | 10 (roughly the same height as the screen) | > 1

Results:
onEndReached is called multiple times on load of FlatList (typically around 10 times)

Notes:

  • I've read around on different issues that it's best to use a number between 0 and 1. Just wanted to document how it behaves when the threshold is greater than 1

Platform | Number of Items | onEndReachedThreshold
--- | --- | ---
iOS | 20 (roughly double the height of the screen) | 0

Results:

  • Works as expected!
  • Still have the momentum issue.

Notes:
This would be my preferred settings, however, the app I have in production is for both iOS and Android. These settings have another issue on Android (see below). I could check for Platform, so that may work.


Platform | Number of Items | onEndReachedThreshold
--- | --- | ---
iOS | 20 (roughly double the height of the screen) | 0.5

Results:

  • Works as expected!
  • Still have the momentum issue.
  • 50% of the time, onEndReached is called once on load of the FlatList (not good)

Platform | Number of Items | onEndReachedThreshold
--- | --- | ---
Android | 5 (roughly half the screen height) | 0

Results:

  • onEndReached is called once on load of the FlatList
  • onEndReached is not called again when scrolling

Platform | Number of Items | onEndReachedThreshold
--- | --- | ---
Android | 10 (roughly the same height as the screen) | 0

Results:
Android works as expected the first time you load more. However, it will not load a second time after you scroll to the bottom with the new data loaded. onEndReached function is not called.


Platform | Number of Items | onEndReachedThreshold
--- | --- | ---
Android | 10 (roughly the same height as the screen) | 0.5 OR 1

Results:

  • onEndReached is called once on load of the FlatList (should not be called until scrolling to the bottom of the list)
  • Once you scroll down and onEndReached is called twice (should only be called once)

Platform | Number of Items | onEndReachedThreshold
--- | --- | ---
Android | 10 (roughly the same height as the screen) | > 1

Results:
onEndReached is called multiple times on load of FlatList (typically around 5 times)

Notes:

  • I've read around on different issues that it's best to use a number between 0 and 1. Just wanted to document how it behaves when the threshold is greater than 1

Platform | Number of Items | onEndReachedThreshold
--- | --- | ---
Android | 20 (roughly double the height of the screen) | 0

Results:

  • onEndReached isn’t called on load (good)
  • onEndReached isn’t called when I scroll to the bottom of the list (bad)

Platform | Number of Items | onEndReachedThreshold
--- | --- | ---
Android | 20 (roughly double the height of the screen) | 0.5

Results:

  • Works as expected!
Bug FlatList iOS Ran Commands

Most helpful comment

`constructor(props){
super(props);
this.state = {
flatListReady:false
}
}

_scrolled(){
this.setState({flatListReady:true})
}

loadMore = () => {
if(!this.state.flatListReady){ return null}

//you can now load more data here from your backend

}
onScroll={this._scrolled.bind(this)}
style={{width:'100%',flexGrow:1}}
ListHeaderComponent={this.headerComponent}
data={this.props.data}
renderItem={this.renderItem}
keyExtractor={(item,index) => index }
onEndReached={(x) => {this.loadMore()}}
onEndReachedThreshold={0.5}

       />

`

All 59 comments

Seeing the same behavior on iOS / React Native 0.48.4

This is also an issue with horizontal FlatLists.

How make not fire onEndReached on load on iOS?
My application entering in infinite loop because of this.

hi (sorry for my English)
my solution:

  1. declare a variable: loadFlag: false,
  2. after first data fetch set loadFlag: true, (for next fetch)
  3. before fetching new data onEndReached check loadFlag, if true then set loadFlag: false and call onEndReached function else don't call onEndReached function (fetch is loading).
  4. after fetch new data from onEndReached function set loadFlag: true. (for next onEndReached)

I found this same issue - try putting onEndReached function call in an anonymous function.

<FlatList
  data={this.state.data}
  keyExtractor={(item, index) => index}
  renderItem={this._renderItem}
  onEndReached={() => this.onEndReached()}
  onEndReachedThreshold={0.5} />

Having the same issue. Im using a boolean flag to circumvent loading the data twice but it's obviously not a very elegant solution.

I have not found a workaround that works...
but so far, if I remove this line, it stops doing the initial onEndReached, but still fires when I actually reach the end.

Since I'm not a contributor, I'm not sure what the implications of removing this is.

Has anyone found a better solution? It's very frustrating to work with FlatList like this.

+1

`constructor(props){
super(props);
this.state = {
flatListReady:false
}
}

_scrolled(){
this.setState({flatListReady:true})
}

loadMore = () => {
if(!this.state.flatListReady){ return null}

//you can now load more data here from your backend

}
onScroll={this._scrolled.bind(this)}
style={{width:'100%',flexGrow:1}}
ListHeaderComponent={this.headerComponent}
data={this.props.data}
renderItem={this.renderItem}
keyExtractor={(item,index) => index }
onEndReached={(x) => {this.loadMore()}}
onEndReachedThreshold={0.5}

       />

`

same problem here

react: ^16.0.0
react-native: 0.50.3

handleLoadMore() {
   if(!this.props.refreshing){
   console.log('handleLoadMore')
   this.props.fetchProducts('down')
   }
}

<FlatList
    numColumns={2}
    data={this.props.products}
    renderItem={({item}) => <Product product={item}/>}
    keyExtractor={(item, index) => index}
    ListFooterComponent={this.renderFooter.bind(this)}
    onRefresh={this.handleRefresh.bind(this)}
    refreshing={this.props.refreshing}
    onEndReached={this.handleLoadMore.bind(this)}
    onEndReachedThreshold={0.5}
/>

+1

+1

Guys any update on the same? I am still facing the issue

I've tried all workarounds and none of them work 100% for me. This is the closest https://github.com/facebook/react-native/issues/14015#issuecomment-310675650 but still has some issue. It doesn't trigger when you scroll to end a second time.

Debouncing onEndReached like this using lodash's debounce or another debounce function works quite well.

constructor(props) {
    super(props);
    this._onEndReached = _.debounce(this._onEndReached, 500);
  }

It stops multiple triggers onLoad (when list is shorter than screen height) and onEndReached.

Thanks for posting this! It looks like you may not be using the latest version of React Native, v0.53.0, released on January 2018. Can you make sure this issue can still be reproduced in the latest version?

I am going to close this, but please feel free to open a new issue if you are able to confirm that this is still a problem in v0.53.0 or newer.

How to ContributeWhat to Expect from Maintainers

This is reproduced in the latest version 0.53.0.
And it can't be pulled refresh if data is empty.

I just reopened this issue, because this bug still occurs in latest version of react-native.

@timwangdev so did you get any solution for the same?

For those of you that use native-base, enclosing FlatList between <Content> causes this issue.

Replacing the <Content> with <View> solved the issue in my case.

I'm wonder if there is any reason to call onEndReached if data is empty (data = [])?

_maybeCallOnEndReached() {
    const {
      data,
      getItemCount,
      onEndReached,
      onEndReachedThreshold,
    } = this.props;
    const {contentLength, visibleLength, offset} = this._scrollMetrics;
    const distanceFromEnd = contentLength - visibleLength - offset;
    if (
      onEndReached &&
      this.state.last === getItemCount(data) - 1 &&
      /* $FlowFixMe(>=0.63.0 site=react_native_fb) This comment suppresses an
       * error found when Flow v0.63 was deployed. To see the error delete this
       * comment and run Flow. */
      distanceFromEnd < onEndReachedThreshold * visibleLength &&
      (this._hasDataChangedSinceEndReached ||
        this._scrollMetrics.contentLength !== this._sentEndForContentLength)
    ) {
      // Only call onEndReached once for a given dataset + content length.
      this._hasDataChangedSinceEndReached = false;
      this._sentEndForContentLength = this._scrollMetrics.contentLength;
      onEndReached({distanceFromEnd});
    }
  }

When your Flatlist component is mounting all checks(most likely) are true if data is empty :

  • onEndReached is true (if you passed ofc)
  • this.state.last === getItemCount(data) -1 is true (last === -1 by default)
  • distanceFromEnd is likely below zero since visibleLength > contentLength when data is empty
    thus distanceFromEnd < onEndReachedThreshold * visibleLength ===true, if onEndReachedThreshold >= 0
  • this._hasDataChangedSinceEndReached === true by default

If there are no such cases when it is necessary to call onEndReached with empty data,
then adding the check for data.length in if statement above will solve the problem i guess.

Hi Guys

Also, I am getting this while running tsc command. (I am using NativeBase)

error TS2322: Type '{ refreshing: boolean; data: DataGridRow[]; renderItem: ({ item, index }: { item: any; index: any...' is not assignable to type 'IntrinsicAttributes & IntrinsicClassAttributes<Component<FlatListProperties<any>, ComponentState>...'.
Type '{ refreshing: boolean; data: DataGridRow[]; renderItem: ({ item, index }: { item: any; index: any...' is not assignable to type 'Readonly<FlatListProperties<any>>'.

Is anyone has any solution for this. I have updated my typescript with the latest version but still, I am getting this. Event the vscode is also showing the same.

"react-native": "0.54.4" and still finding this issue, quire annoying to be honest

Hi All,

My solution here is to check first whether the Flatlist was already scrolled or not by using onScrollEndDrag property of Flatlist.

onEndReached will still be called for the first time, but you can prevent it before data fetching since the list was not scrolled yet (in my example its on _handleLoadMore_ function).

Please see my example code below.

constructor(props) {
super(props);
this.state = {isListScrolled: false};
}

handleLoadMore = () => {
if (isListScrolled) {
(Load more list....)
}
}

onScrollEndDrag={this.setState({isListScrolled: true})}
keyExtractor={(item, index) => index.toString()}
onEndReached={this.handleLoadMore}>

My react native version "react-native": "^0.52.2"

@patrickleemsantos
I think you will need to use something like this or else I get errors.
onScrollEndDrag={() => {this.setState({ isListScrolled: true }); }}

Also seeing this on "react-native": "0.54.4". Super duper annoying.

+1

Thanks for posting this! It looks like your issue may refer to an older version of React Native. Can you reproduce the issue on the latest release, v0.55?

Thank you for your contributions.

I can confirm that it still happens on 0.55 (specifically 0.55.2)

I am getting the error on 0.55 also

it's still happen

my environment

Packages: (wanted => installed)
  react: ^16.3.1 => 16.4.1
  react-native: ^0.55.4 => 0.55.4

+1

+1

it's still happen in 0.55.4.
I think setting Datas leads to FlatView rendering and trigger the event "onEndReached", I solved it by doing this:


componentDidMount() {
        this.isScrolled = false;       
}
getData = () => {
        // this.setState({data: [.....]});
        setTimeout(() => {
                this.isScrolled = true;
        }, 100);
}
onEndReached = () => {
        if (!this.isScrolled) {
            return null;
        }
}

And that's why react in itself is really a problem for professional applications that don't have 500 devs to workaround stuff like airbnb had to (which are now moving away from RN again, like many others will). We won't see any fix from facebook if they don't have the same issue somewhere. They simply don't care.

Same issue in newest versions without any ui library. It also happens when bounce is deactivated.
It actually goes into an infinite loop until everything has been loaded. At no point the list has been touched. That's very likely because the flatlist doesn't read it's height correctly, but I'm just using View -> FlatList with nothing else at all.

System:
  OS: macOS 10.14
  CPU: x64 Intel(R) Core(TM) i9-8950HK CPU @ 2.90GHz
  Memory: 13.74 GB / 32.00 GB
  Shell: 3.2.57 - /bin/bash
Binaries:
  Node: 8.11.3 - /usr/local/bin/node
  Yarn: 1.10.1 - /usr/local/bin/yarn
  npm: 5.6.0 - /usr/local/bin/npm
  Watchman: 4.9.0 - /usr/local/bin/watchman
SDKs:
  iOS SDK:
    Platforms: iOS 12.0, macOS 10.14, tvOS 12.0, watchOS 5.0
IDEs:
  Android Studio: 3.1 AI-173.4907809
  Xcode: 10.0/10A255 - /usr/bin/xcodebuild
npmPackages:
  react: 16.5.0 => 16.5.0 
  react-native: 0.57.2 => 0.57.2 
npmGlobalPackages:
  react-native-cli: 2.0.1
  react-native-git-upgrade: 0.2.7

For those of you that use native-base, enclosing FlatList between <Content> causes this issue.

Replacing the <Content> with <View> solved the issue in my case.

There could also be issues when FlatList is inside another ScrollView.

React Native 0.57.2 and issue persists when the FlatList is inside a <ScrollView>. Removing the ScrollView solves the problem.

@coocon It works.
Latest RN 0.57.4

Any updates? Issues exist more that 1 year, it easily reproduced on latest RN by placing FlatList inside of ScrollView and still not fixed

Even I didn't put the FlatList inside ScrollView, the problem still persists.
RN 0.57.0

For those of you that use native-base, enclosing FlatList between <Content> causes this issue.
Replacing the <Content> with <View> solved the issue in my case.

There could also be issues when FlatList is inside another ScrollView.

You saved my life man. 👍 💯

is there anybody care this problem.

RN 0.59.2 and this is still a problem.

This is a very mind-blowing thing for them to fix. I am sure we gonna have to wait another 2-3 years for it.

Pretty annoying.

Still has onEndReached bug, can not works well as the document said.

`constructor(props){
super(props);
this.state = {
flatListReady:false
}
}

_scrolled(){
this.setState({flatListReady:true})
}

loadMore = () => {
if(!this.state.flatListReady){ return null}

//you can now load more data here from your backend

}
onScroll={this._scrolled.bind(this)}
style={{width:'100%',flexGrow:1}}
ListHeaderComponent={this.headerComponent}
data={this.props.data}
renderItem={this.renderItem}
keyExtractor={(item,index) => index }
onEndReached={(x) => {this.loadMore()}}
onEndReachedThreshold={0.5}

       />

`

this works for me

the bug is still exist anyway, using Expo 34 --> RN 0.59.8

Edit: using FlatList from gesture-handler seems better

https://github.com/ifsnow/react-native-infinite-flatlist-patch

I made a patch library to help with this issue. I hope this helps.

the problem still exists in latest version of RN

Sorry for the bug and the radio silence here.

I finally dug into this and I believe the issue is indeed a race between onLayout and onContentSizeChange, which in general should be fine because there is no expectation of ordering between the two, and only causes issues with certain configurations.

The bug can be triggered if initialNumToRender is smaller than needed to fill past the onEndReachedThreshold (say the default, 10, is only 580px tall, but it takes 15 to reach the threshold). This will cause an incrementally render of more items to try and fill the viewport. The problem is that if the onLayout comes back before the first onContentSizeChange, it will first do the state increment to render 20 items and then the stale onContentSizeChange callback from 10 items will fire and we'll think that the content size for 20 items is 580px when in fact it's 1160px (which is past the threshold). If those 20 items are also all of our available data, then we'll call onEndReached because we think we've rendered everything and are still within the onEndReachedThreshold.

The fundamental problem here is the system getting confused when a stale async onContentSizeChange comes in after increasing state.last. I wish there was a concrete timeframe, but Fabric will give us more flexibility to do things synchronously so hopefully we can avoid class of issues once that roles out.

There are a few workarounds you can try while you wait for a proper fix to be released:

1) Provide the getItemLayout prop so the list doesn't have to rely on async layout data (you should do this whenever possible anyway for better perf). e.g. for the original snack example, you can just add getItemLayout={(d, index) => ({length: 58, offset: 58 * index, index})} since all the rows are height 58 and the issue will no longer repro. Note this is fragile and must be kept in sync with UI changes, a11y font scaling, etc - a more robust approach could be to render a single representative row offscreen and measure it with onLayout then use that value.
2) If getItemLayout is not feasible to compute for your UI, increase initialNumToRender to cover the onEndReachedThreshold.
3) And/or add your own logic to protect against extra calls to onEndReached as others have suggested.

This should be fixed in https://github.com/facebook/react-native/commit/8ddf231306e3bd85be718940d04f11d23b570a62, but it's still possible there are some related issues. Our plan is still to wait for fabric before doing a major overhaul / re-write of lists.

Hi guys, I am having the same problem too, but I have noticed this happening. On initial load when the array of data is of length 0 (no data), the onEndReached doesn't trigger. But on subsequent navigation to this screen and the array is already populated with some data, I see that it triggers onEndReached (with no scroll, on load)
The array is kept cuz I didn't want clear the array in reducer upon FETCH_DATA action, cuz if there is some data previously fetched already, should be shown instead of the loader.

For those of you that use native-base, enclosing FlatList between causes this issue.

Replacing the with solved the issue in my case

This saves my day! Thanks!

It's not fixed yet and it's a horrible bug, I tried the solutions like flag and stuff but on different items for flatlist I got different results so the whole thing is useless

Still having this bug.

I literally deleted this feature from my app and added some item add the footer saying load more items...clicking that would load more items.Well it's terrible but at least I know it always works. Unlike the onEndReached which does wathever it wants whenever it wants and doesn't do anything it doesn't want to do.

Still having the bug, it has been literally 3 years!!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

lazywei picture lazywei  ·  3Comments

grabbou picture grabbou  ·  3Comments

phongyewtong picture phongyewtong  ·  3Comments

janmonschke picture janmonschke  ·  3Comments

DreySkee picture DreySkee  ·  3Comments