React-native: [FlatList] onEndReached triggered 2 times

Created on 17 May 2017  ·  29Comments  ·  Source: facebook/react-native

ref={(flatList)=>this._flatList = flatList}
ListHeaderComponent={this._header}
ListFooterComponent={this._footer}
ItemSeparatorComponent={this._separator}
renderItem={this._renderItem}
getItemLayout={(data,index)=>({length: ITEM_HEIGHT, offset: (ITEM_HEIGHT+2) * index, index})}
onEndReachedThreshold={0.5}
onEndReached={(info)=>{
console.log(JSON.stringify(info));
}}
data={data}
/>

When the scroll speed is fast enough,onEndReached triggered 2 times or more times

Is there any solution?

Locked

Most helpful comment

OK, here is my solution for how to avoid the second onEndReached call with the bouncing effect enabled on iOS:

  1. Add onMomentumScrollBegin prop to your FlatList declaration.
        <FlatList
          data={this.props.data}
          onEndReached={...}
          onEndReachedThreshold={0.5}
          ...
          onMomentumScrollBegin={() => { this.onEndReachedCalledDuringMomentum = false; }}
        />
  1. Modify your onEndReached callback to trigger data fetching only once per momentum.
  onEndReached = () => {
    if (!this.onEndReachedCalledDuringMomentum) {
      this.props.fetchData();
      this.onEndReachedCalledDuringMomentum = true;
    }
  };

Tested with RN 0.44.0.

All 29 comments

mine doesn't even trigger.

@parkerproject
My paltform is ios 10.3,mac osx 10.12.5

@fengshanjian
Yep, got the same - fires 2 times. But if you are using redux or even state - you can use something like this

onEndReached={(info)=>{
if (this.state.loading===false){
console.log(JSON.stringify(info));
}
}}

Same issue my side. Seems to be a bug in the ScrollView bouncing feature on iOS (when you over scroll on iOS the ScrollView scrolls past the content and bounces back to the end of the content by default).

Setting bounces={false} solved onEndReached being called twice for me. Still not an ideal solution though as you lose that nice smooth/fluid feel in the UX.

Hey, thanks for reporting this issue!

It looks like your description is missing some necessary information. Can you please add all the details specified in the template? This is necessary for people to be able to understand and reproduce the issue being reported.

I'm definitely seeing the second onEndReached call triggered by the bouncing effect on iOS. Adding bounces={false} to the FlatList fixed it for me. I think ideally, RN should not call onEndReached while the list is in the "bouncing" state. Not sure how doable is this though.

OK, here is my solution for how to avoid the second onEndReached call with the bouncing effect enabled on iOS:

  1. Add onMomentumScrollBegin prop to your FlatList declaration.
        <FlatList
          data={this.props.data}
          onEndReached={...}
          onEndReachedThreshold={0.5}
          ...
          onMomentumScrollBegin={() => { this.onEndReachedCalledDuringMomentum = false; }}
        />
  1. Modify your onEndReached callback to trigger data fetching only once per momentum.
  onEndReached = () => {
    if (!this.onEndReachedCalledDuringMomentum) {
      this.props.fetchData();
      this.onEndReachedCalledDuringMomentum = true;
    }
  };

Tested with RN 0.44.0.

hi @grin can you pls refer me some document about the function onMomentumScrollBegin? i cannot find any on http://facebook.github.io/react-native/releases/0.46/docs/flatlist.html

FlatList is just wrapper of the ScrollView

Hey, thanks for reporting this issue!

It looks like your description is missing some necessary information, or the list of reproduction steps is not complete. Can you please add all the details specified in the template? This is necessary for people to be able to understand and reproduce the issue being reported.

I am going to close this, but feel free to open a new issue with the additional information provided. Thanks!

many thanks @grin. i faced with multiple call onEndReached and your solution decrease my api call by 2. I fixed it by adding load state validation:
if (!this.onEndReachedCalledDuringMomentum && !this.state.loading)
works for me

@ezha86 Can you look at the code?

I've been having a couple different issues with onEndReached. I recently created an issue with a very detailed breakdown of everything here: https://github.com/facebook/react-native/issues/16067.

Hope this is helpful to someone.

My react-native version is 0.44


I have in trouble with onEndReached called much times.
and thanks for friends answers,I get idea from onMomentumScrollBegin and onMomentumScrollEnd.
Here is my solution for share

                    onEndReachedThreshold={0}
                    onEndReached={(info)=>{
                        console.log('onEndReached',info);
                       //need setState ,  otherwise this callback called only once
                        this.setState({
                            // empty this content is also work
                        })
                    }}

                    onMomentumScrollBegin={()=>{
                        console.log('onMomentumScrollStart...')
                    }}
                    onMomentumScrollEnd={()=>{
                        console.log('onMomentumScrollEnd!!!')
                    }}



when you drag list to bottom , you will see log below.

onMomentumScrollStart...
'onEndReached', { distanceFromEnd: -52 }
'onEndReached', { distanceFromEnd: -23 }
'onEndReached', { distanceFromEnd: -8.5 }
.
.
.
onMomentumScrollEnd!!!

so , my solution is do something in onMomentumScrollEnd .
when trigger onEndReached, you set this.shouldLoadMore = true,
when your finger leave list , onMomentumScrollEnd will be called.

 onMomentumScrollEnd={()=>{
         console.log('onMomentumScrollEnd!!!')
         if(this.shouldLoadMore = true){
            //load datas
           this.shouldLoadMore = false
         }
}}




BTW but I still have a question
if not set state or render again , onEndReached called only once.like this:

onEndReached={(info)=>{
                        console.log('onEndReached',info);
                    }}

dose someone kowns the reason ?

@grin solution is good, but when the data is small, there is no scroll bar, it still triggered my onLoadMore() method.

My solution:

class ListContainer extends PureComponent {
  constructor(props) {
    super(props);
    this.state = {
      selected: (new Map()),
    };
  }

  _keyExtractor = (item, index) => item.ID || item.id;

  _onPressItem = (id, code) => {
    // updater functions are preferred for transactional updates
    this.setState((state) => {
      // copy the map rather than modifying state.
      const selected = new Map(state.selected);
      selected.set(id, !selected.get(id)); // toggle
      return {selected};
    });

    let {onPress} = this.props;
    if (onPress) onPress(id, code);
  };

  _renderItem = ({item}) => {
    if (!!item['loading']) {
      return (
        <ActivityIndicator style={[styles.indicator]}/>
      )
    } else if (!!item['noMoreData']){
      return (
        <Text style={{textAlign:'center', color:Colors.DeepGray, fontSize:16, paddingVertical:5,}}>没有更多数据...</Text>
      )
    } else if (item['empty']){
      return (
        <Text style={{textAlign:'center', color:Colors.DeepGray, fontSize:16, paddingVertical:5,}}>没有数据...</Text>
      )
    }
    else {
      return (
        <ListItem
          id={item.ID || item.id}
          item={item}
          onPressItem={this._onPressItem}
          type={this.props.itemType}
          selected={!!this.state.selected.get(item.id)}
          title={item.title}
        />
      )
    }
  };

  _separator = () => {
    return <View style={{height: 1 / PixelRatio.get(), backgroundColor: Colors.DeepGray, marginHorizontal: 10}}/>;
  };

  _onRefresh = () => {
    let {onRefresh} = this.props;
    onRefresh && onRefresh();
  };

  _onEndReached = (info)=>{
    console.log('1-trigger list onEndReached !', info);
    if (info.distanceFromEnd >= -10) {
      this.props.onLoadMore && this.props.onLoadMore();
    }
  };

  render() {
    return (
      <FlatList
        style={{flex: 1}}
        onEndReachedThreshold={0.05}
        onEndReached={this._onEndReached}
        data={this.props.data}
        extraData={this.state}
        keyExtractor={this._keyExtractor}
        renderItem={this._renderItem}
        ItemSeparatorComponent={this._separator}
        refreshing={this.props.refreshing}
        onRefresh={this.props.onRefresh ? this._onRefresh : null}
      />
    );
  }
}
<ListContainer data={orders} refreshing={isFetching} onRefresh={this._refreshList}
               onLoadMore={() => {
                 if (!isFetching && !isPaging && hasMoreData) {
                   dispatch(fetchOrdersPage())
                 }
               }}
               onPress={(id, orderNo) => {
                 this._openDialog();
                 this.activeOrderNo = orderNo;
               }}/>

May be it is not a good solution, but works fine in my scenario. For reference only

Try this
onEndReachedThreshold={0.01}

Tried all the above mentioned solution, nothing helped. Still facing this multiple call issue in IOS & Android.

Was also happening with me, try keeping a flag to stop making request.

if (!this.state.loading) {
    this.setState({loading:true});
    this.setState(state => ({ page: state.page+1}), this.makeNetworkRequest());
}

Then I set the state to loading false after completing of request

`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}

           />

`

onEndReached is not working as expected. It's not triggering continuously when we reached the end.
FYI: I am using the native-base components.

using setTimeout for setState, eg.
setTimeout(() => this.setState({ arr: [ ...newArr ] }), 3000);

@gougoushan Not working for me

don't use onEndReached
react-native-refresh-list-view

  onScroll = ({nativeEvent}) => {
        let previousOffsetY = 0;
        if (this.nativeEvent) {
            previousOffsetY = this.nativeEvent.contentOffset.y;
        }
        const offsetY = nativeEvent.contentOffset.y;

        /**
         * 判断是否为用户拖拽
         */
        if (this.isResponder) {
            /**
             * 判断为上拉并且滚动到底部
             */
            if ((offsetY - previousOffsetY) > 0 && offsetY >= (nativeEvent.contentSize.height + nativeEvent.contentInset.bottom - nativeEvent.layoutMeasurement
                    .height)) {
                if (this.shouldStartFooterRefreshing()) {
                    this.props.onFooterRefresh && this.props.onFooterRefresh(RefreshState.FooterRefreshing);
                }
            }
        }

        this.nativeEvent = nativeEvent;
    }

@GaoYuJian You are a hero!!

This is my solution:

<FlatList
  ...
  refreshing={this.state.refreshing}
  onRefresh={this._onRefresh}
  onEndReached={this._onEndReached}
  onEndReachedThreshold={0.2}
  onScrollBeginDrag={() => {
    console.log('onScrollBeginDrag');
    this.canAction = true;
  }}
  onScrollEndDrag={() => {
    console.log('onScrollEndDrag');
    this.canAction = false;
  }}
  onMomentumScrollBegin={() => {
    console.log('onMomentumScrollBegin');
    this.canAction = true;
  }}
  onMomentumScrollEnd={() => {
    console.log('onMomentumScrollEnd');
    this.canAction = false;
  }}
/>
_onRefresh = () => {
  console.log('_onRefresh');
  if(!this.canAction) return;
  ...
  };

_onEndReached = () => {
  console.log('_onEndReached');
  if(!this.canAction) return;
  ...
};

For some reasons my onMomentumScrollBegin is not being fired. Is this happening to anybody else?

I have solved it with using debounce from lodash

first import debounce from 'lodash.debounce'

and then just add this to your constructor

this._onEndReached = debounce(this._onEndReached, 500);

Is this going to be fixed in the next release or is the hack necessary?

Was this page helpful?
0 / 5 - 0 ratings