beforesemicolon / flatlist-react

A helpful utility component to handle lists in react like a champ
MIT License
95 stars 17 forks source link

Loading more items on every scroll event #63

Closed pribeh closed 4 years ago

pribeh commented 4 years ago

When we setup the flatlist with hasmoreitems and pagination we seem to be experiencing an odd loading behaviour on scroll. If the cursor is outside the region of the flatlist and you scroll all the way to the bottom of the list no event is triggered to load more items. This may be a totally separate issue but we do not know why this is happening. However, when you scroll inside the region of the flatlist it seems like the hasmoreitems fires on every scroll event loading more items to the list each time.

To reproduce this:

this.state = {
      hasMoreItems: false,
}

getUsersProfilesList = async () => {
  return await client.service("users").find({
      query: {
        $limit: 10,
        $skip: this.state.profiles.length,
      }
    }).then((profilesPage) => {
      this.setState(prevState => ({
        hasMoreItems: profilesPage.data.length === profilesPage.limit,
        profiles: [...prevState.profiles, ...profilesPage.data],
        loading: false
    }))
  })
}

render() { 
            <FlatList
              list={this.state.profiles}
              renderItem={this.renderUserProfile}
              renderWhenEmpty={() => <div style={{ flex: 1, textAlign: "center" }}>List is empty!</div>}
              hasMoreItems={this.state.hasMoreItems}
              loadMoreItems={this.getUsersProfilesList}
            />
}

Here's the full example:


import React, { Component } from "react";
import Dropdown from 'react-dropdown';
import 'react-dropdown/style.css';
import FlatList from 'flatlist-react';
import UserProfileComponent from "../components/UserProfileComponent";
import UserFlaggedPost from "../components/UserFlaggedPost";
import moment from 'moment';

const feathers = require('@feathersjs/feathers');
const socketio = require('@feathersjs/socketio-client');
const authentication = require('@feathersjs/authentication-client');
const io = require('socket.io-client');

const socket = io("https://dev.mysite.com", {
  transports: ['websocket'],
  upgrade: false,
});

// Initialize our Feathers client application through Socket.io
// with hooks and authentication.
const client = feathers();

client.configure(socketio(socket));
client.configure(authentication({
  storage: localStorage
}))

const options = [];

class User extends Component {
  constructor(props) {
    super(props)
    this.state = {
      hasMoreItems: false,
      offset: 0,
      loading: true,
      profiles: [],
      value: 1,
      usersFlagged: [ ],
    };

    this.defaultOption = "Select date...";
    for (let i = 0; i < 13; i++) {
      if (i !== 12) {
        options.push({
          value: i,
          label: moment().subtract(i, "month").startOf("month").format('MMM') + '-' + moment().subtract(i - 1, "month").startOf("month").format('MMM') + ' ' + moment().subtract(i, "month").year()
        });
      } else {
        options.push({
          value: i,
          label: "None"
        });
      }
    }
  }

  async componentDidMount() {
    await client.authenticate({
      strategy: "local",
      uname: '...',
      pword: '...',
      extraProps: {
        // tells the server what functionality the client's app version supports
        apiVersion: '2',
      },
    });
    await this.getUsersProfilesList();
  }

  renderUserProfile = (item, idx) => {
    return (
      <UserProfileComponent key={idx} item={item} />
    );
  }

  renderUserFlaggedPost = (item, idx) => {
    return (
      <UserFlaggedPost key={idx} item={item} />
    );
  }

  getUsersProfilesList = async () => {
    //console.log("******************************* before skip: " + this.state.profiles.length + ", this.hasMoreItems=" + this.state.hasMoreItems)
    return await client.service("users").find({
      query: {
        $limit: 10,
        $skip: this.state.profiles.length,
      }
    }).then((profilesPage) => {
      this.setState(prevState => ({
        hasMoreItems: profilesPage.data.length === profilesPage.limit,
        profiles: [...prevState.profiles, ...profilesPage.data],
        loading: false
      }))
      //console.log("******************************* AFTER this.state.profiles.length: " + this.state.profiles.length + ", returned skip: " + profilesPage.skip + ", this.hasMoreItems=" + this.state.hasMoreItems)
    })
      .catch(error => console.log(error.message));
  }

  getUsersProfilesByMonth = async (start, end) => {
    this.setState({ profiles: [] })
    return await client.service("users").find({
      query: {
        createdAt: {
          $lte: end,
          $gte: start,
        },
        $limit: 10,
        $skip: this.state.profiles.length,
      }
    }).then((profilesData) => {
      this.setState(prevState => ({
        hasMoreItems: profilesData.data.length === profilesData.limit,
        profiles: [...prevState.profiles, ...profilesData.data],
        loading: false
      }))
    })
  }

  searchByMonth = async (option) => {
    let timeStart = 0;
    let timeEnd = 0;

    for (let i = 0; i < 13; i++) {
      if (option.value === i) {
        if (i !== 12) {
          this.defaultOption = option.label;
          timeStart = moment().subtract(i, 'months').startOf('month').valueOf();
          timeEnd = moment().subtract(i, 'months').endOf('month').valueOf();
          //console.log ("timeStart = " + moment().subtract(i, 'months').startOf('month').format("YYYY-MM-DD"));
          //console.log ("timeEnd = " + moment().subtract(i, 'months').endOf('month').format("YYYY-MM-DD"));
          await this.getUsersProfilesByMonth(timeStart, timeEnd);
        } else {
          await this.setState({ profiles: [] })
          await this.getUsersProfilesList();
          this.defaultOption = "Select date...";
        }
      }
    }
  }

  render() {

    return (
      <div style={this.state.profiles.length < 3 ? { width: 950, height: window.innerHeight } : { width: 950 }}>
        <div style={{ flex: 1, flexDirection: "row", display: "flex" }}>
          <img src={require("../images/single-02.svg")} alt="user" style={{ height: 25, width: 25, paddingLeft: 30, paddingRight: 10, paddingTop: 30, filter: "brightness(0) saturate(100%)" }} />
          <h2 style={{ paddingTop: 10, color: "#000000", fontWeight: 600 }}>Users</h2>
        </div>
        <div style={{ flex: 1, flexDirection: "row", display: "flex" }}>
          <div style={{ flex: 1 }}>
            <div style={{ flex: 1, flexDirection: "row", display: "flex", justifyContent: "space-between" }}>
              <p style={{ fontSize: 16, fontWeight: 600, paddingLeft: 50, paddingTop: 10 }}>New</p>
              <Dropdown
                options={options}
                onChange={this.searchByMonth}
                value={this.defaultOption}
                placeholder={this.defaultOption} />
            </div>
            <FlatList
              list={this.state.profiles}
              renderItem={this.renderUserProfile}
              renderWhenEmpty={() => <div style={{ flex: 1, textAlign: "center" }}>List is empty!</div>}
              hasMoreItems={this.state.hasMoreItems}
              loadMoreItems={this.getUsersProfilesList}
            />
          </div>
          <div style={{ flex: 1 }}>
            <div style={{ flex: 1, flexDirection: "row", display: "flex", justifyContent: "space-between" }}>
              <p style={{ fontSize: 16, fontWeight: 600, paddingLeft: 50, paddingTop: 10 }}>Flagged</p>
              <Dropdown
                options={options}
                onChange={this.searchByMonth}
                value={this.defaultOption}
                placeholder="Select Date" />
            </div>
            <FlatList
              list={this.state.usersFlagged}
              renderItem={this.renderUserFlaggedPost}
              renderWhenEmpty={() => <div style={{ flex: 1, textAlign: "center" }}>There are no results for this date. Try again.</div>}
            />
          </div>
        </div>
      </div>
    );
  }
}

export default User;

**Desktop (please complete the following information):**
 - OS: Windows/Mac
 - Browser: Chrome
 - Version: 84.0.4147.105 (Official Build) (64-bit)
- React and React Dom 16.8.0
ECorreia45 commented 4 years ago

Hey @pribeh thanks for this report, before i continue with the investigation i have notice couple things in your code that could be problematic(may not be the cause of this issue which i'll take a look at).

also: What FlatList version are you using?

try that fix and get back to me while i investigate the cursor and any related issue with pagination. sorry for this

pribeh commented 4 years ago

Thanks @ECorreia45 . The adjustment you suggested doesn't yield any items loaded on scroll – no additional results are returned.

The console spits out:

Users.js:88******************************* before skip: 0, this.hasMoreItems=false
Users.js:88******************************* before skip: 10, this.hasMoreItems=true
Users.js:100 ******************************* AFTER this.state.profiles.length: 10, returned skip: 0, this.hasMoreItems=true
Users.js:100 ******************************* AFTER this.state.profiles.length: 20, returned skip: 10, this.hasMoreItems=true
Users.js:88 ******************************* before skip: 20, this.hasMoreItems=true
Users.js:100 ******************************* AFTER this.state.profiles.length: 30, returned skip: 20, this.hasMoreItems=true
Users.js:88 ******************************* before skip: 30, this.hasMoreItems=true
Users.js:100 ******************************* AFTER this.state.profiles.length: 40, returned skip: 30, this.hasMoreItems=true
Users.js:88 ******************************* before skip: 40, this.hasMoreItems=true
Users.js:100 ******************************* AFTER this.state.profiles.length: 50, returned skip: 40, this.hasMoreItems=true

We're using Flatlist ^1.4.2

ECorreia45 commented 4 years ago

Thanks for this. Will keep you posted on this.

ECorreia45 commented 4 years ago

Hey, @pribeh I took a look at this and could not find anything that may have caused the problem you are experiencing.

By taking a look at your code again, a couple of things jumped at me again:

1 - your logs make sense since it should load more if the hasMoreItems is true as your logs indicate. I would be surprised if it was false and kept calling. I still think something about yout hasMoreItems state set is not quiet right because profilesPage.data.length will be 10 or less according to $limit: 10. profilesPage.limit will be the max data from your database. Can you run this and print the profilesPage.limit. I think you should check if hasMoreItems: prevState.profiles.length === profilesPage.limit instead. also be careful with the way you are logging things, react setState is asynchronous so use it second parameter to log things after like this.setState(prevState => ({....}), () => {console.log('****** AFTER ....').

2 - display flex is interfering with the scrolling. display flex force things to fit and flatlist relies and scrollable containers so make sure the container with the flatlist items have "overflow: auto". You have flex: 1 everywhere which forces box to take available space to fit.

3 - setState does not return promise so you cant await on it. try this instead:

this.setState({ profiles: [] }, async () => {
          await this.getUsersProfilesList();
          this.defaultOption = "Select date...";
})

4 - there are a lot of async and await going around instead of using promise chains and natural react lyfescycle methods which makes it hard to follow the logic of your application and guess what is probably doing.

If you can get in touch with me i would love to help you further but i could not find anything wrong beside the way it is being used. sorry.