FlatList onEndReached being called multiple times [duplicate]
Asked Answered
H

6

44

I'm making a react native project where user can search images using Flickr API, Everything else is working fine but the problem i'm having while implementing pagination. I have used FlatList's onEndReached to detect when user has scrolled to the end on the list, but the problem is onEndReached is being called multiple times(including one during the first render). I have even disabled bounce as said here but it's still being called more than once

 export default class BrowserHome extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      isLoading: false,
      tagParam: "cat",
      pageNum: -1,
      data: [],
      photosObj: ""
    };
  }

  componentDidMount() {
    this.setState({
      isLoading: true
    });
    try {
      this.makeRequest();
    } catch {
      console.log("error has occurred");
    }
  }

  makeRequest = () => {
    const { tagParam, pageNum } = this.state;
    let url = `https://api.flickr.com/services/rest/? 
               method=flickr.photos.search
               &api_key=${apiKey}&format=json&tags=${tagParam}
               &per_page=30&page=${pageNum + 1}&nojsoncallback=1`;
    fetch(url, {
      method: "GET"
    })
      .then(response => response.json())
      .then(responseJSON => {
        this.setState({
          data: this.state.data.concat(responseJSON.photos.photo),
          isLoading: false,
          pageNum: responseJSON.photos.page
        });
      })
      .catch(error => {
        console.log(error);
        this.setState({ isLoading: false });
        throw error;
      });
  };

  render() {
    if (this.state.isLoading) {
      return <ActivityIndicator animating={true} size="large" />;
    }

    return (
      <View
        style={{
          flex: 1,
          height: 200,
          justifyContent: "flex-start",
          width: screenSize.width,
          backgroundColor: "black"
        }}
      >
        <Text>This is browserhome</Text>
        <FlatList
          style={{
            width: screenSize.width
          }}
          numColumns={3}
          data={this.state.data}
          keyExtractor={item => item.id}
          bounces={false}
          onEndReachedThreshold={1}
          onEndReached={({ distanceFromEnd }) => {
            this.loadMoreItem();
            alert("end reached call");
          }}
          renderItem={({ item, index }) => (
            <>
              <ImageTile imageURL={this.createImageURL(item)} />
            //  <Text style={{ color: "white" }}>
             //   {index}
             //   {console.log(index)}
             // </Text>
            </>
          )}
        />
      </View>
    );
  }

  createImageURL(item) {
    let server = item.server,
      id = item.id,
      secret = item.secret;
    let urlString = `https://farm${
      item.farm
    }.staticflickr.com/${server}/${id}_${secret}_s.jpg`;
    return urlString;
  }

  loadMoreItem() {
    this.makeRequest();
  }
}
Hurlee answered 21/11, 2018 at 9:3 Comment(2)
1. Add onMomentumScrollBegin prop to your FlatList declaration. <FlatList data={this.props.data} onEndReached={...} onEndReachedThreshold={0.5} ... onMomentumScrollBegin={() => { this.onEndReachedCalledDuringMomentum = false; }} /> 2. Modify your onEndReached callback to trigger data fetching only once per momentum. onEndReached = () => { if (!this.onEndReachedCalledDuringMomentum) { this.props.fetchData(); this.onEndReachedCalledDuringMomentum = true; } }; ```Rutan
#41297534Oahu
S
36

This solution worked for me. Add onMomentumScrollBegin and modify onEndReached in FlatList Component.

<FlatList
style = { ...}
data = {data}
initialNumToRender = {10}
onEndReachedThreshold = {0.1}
onMomentumScrollBegin = {() => {this.onEndReachedCalledDuringMomentum = false;}}
onEndReached = {() => {
    if (!this.onEndReachedCalledDuringMomentum) {
      this.retrieveMore();    // LOAD MORE DATA
      this.onEndReachedCalledDuringMomentum = true;
    }
  }
}
/>
Specs answered 13/3, 2020 at 7:9 Comment(0)
S
19

You would be best using onEndReached to set a boolean true, and then using onMomentumScrollEnd based on that.

onEndReached={() => this.callOnScrollEnd = true}
onMomentumScrollEnd={() => {
  this.callOnScrollEnd && this.props.onEndReached()
  this.callOnScrollEnd = false
}
Swede answered 14/1, 2020 at 10:54 Comment(0)
H
9

Here is how I solved my problem:

Here is my initial state:

state = {
  onEndReachedCalledDuringMomentum: true,
  lastLoadCount: 0,
}

This is my FlatList

<FlatList
   keyboardShouldPersistTaps="always"
   style={...}
   data={this.state.searchResults}
   extraData={this.state}
   bounces={false}
   renderItem={({ item, index }) =>
         <SearchResultView
            uriSsource={item.image}
            itemIndex={index}
            name={item.name}
          />
   }
   showsVerticalScrollIndicator={false}
   keyExtractor={this._keyExtractor}
   numColumns={2}
   onEndReached={() => this._loadMoreData()}
   onEndReachedThreshold={0.01}
   ListFooterComponent={this._renderSearchResultsFooter}
   onMomentumScrollBegin={() => this._onMomentumScrollBegin()}
/>

Here are the functions I am calling:

// Key Extractor
    _keyExtractor = (item, index) => item.id;
// Check if list has started scrolling
    _onMomentumScrollBegin = () => this.setState({ onEndReachedCalledDuringMomentum: false });
// Load more data function
    _loadMoreData = () => {
            if (!this.state.onEndReachedCalledDuringMomentum) {
                this.setState({ onEndReachedCalledDuringMomentum: true }, () => {

                    setTimeout(() => {
                        if (this.state.lastLoadCount >= 20 && this.state.notFinalLoad) {
                            this.setState({

                                page: this.state.page + 1,
                            }, () => {
                                // Then we fetch more data;
                                this._callTheAPIToFetchMoreData();
                            });
                        };
                    }, 1500);
                });
            };
        };
// Show your spinner
    _renderSearchResultsFooter = () => {
            return (
                (this.state.onEndReachedCalledDuringMomentum && this.state.lastLoadCount >= 20 && this.state.notFinalLoad) ?
                    <View style={{ marginBottom: 30, marginTop: -50, alignItems: 'center' }}>
                        <ActivityIndicator size="large" color="#e83628" />
                    </View> : null
            )
        }

Once I get data, inside of _callTheAPIToFetchMoreData(), I update the state like this:

this.setState({
  lastLoadCount: results.length,
  onEndReachedCalledDuringMomentum: results.length >= 20 ? true : false,
  notFinalLoad: results.length >= 20 ? true : false
}

Happy coding.

Helminthology answered 22/3, 2019 at 16:26 Comment(0)
H
7

The reason of triggering onEndReached multiple times is because you have not set initialNumToRender properly.

onEndReached is triggered in this _maybeCallOnEndReached in VirtualizedList.

  _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 &&
      distanceFromEnd < onEndReachedThreshold * visibleLength &&
      (this._hasDataChangedSinceEndReached ||
        this._scrollMetrics.contentLength !== this._sentEndForContentLength)
    ) {
    ...

if contentLength(the length of content rendered at once) and visibleLength(usually the screen height) is close, distanceFromEnd can be very small, thus distanceFromEnd < onEndReachedThreshold * visibleLength can always be true. By setting initialNumToRender and control the size of contentLength, you can avoid unnecessary onEndReached call.

Here's an example. If you render 10 items(this is the default props of initialNumToRender) of 70 px cells at the initial render, contentLength becomes 700. If the device you are using is iPhoneX then visibleLength is 724. In that case distanceFromEnd is 24 and this will trigger onEndReached unless you set onEndReachedThreshold less than 0.03.

Hylton answered 27/8, 2019 at 20:16 Comment(0)
D
1

You just need to set onEndReachedThreshold as a rate of visibleLength. So you just need to set it as a number smaller than 1. ZERO for example or 0.5 then it should work!!!!!

Let me know if that worked for you.

Developer answered 21/11, 2018 at 9:56 Comment(0)
J
-4

you can disable scrolling while data receiving. in FlatList set scrollEnabled false while data loading.

scrollEnabled={!this.props.loadingData}
Joy answered 30/10, 2019 at 15:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.