Search code examples
javascriptreactjsreact-infinite-scroll-componentreact-infinite-scroll

React infinite scroll component - dataLength not working


I have a function newsFeed which is called by useEffect and makes a request to an endpoint to fetch all posts made by the logged in user as well as any users they follow, and then saves the data response to the state:

const [posts, setPosts] = useState([])
const [page, setPage] = useState(1)

useEffect(() => {
    newsFeed();
}, []

const newsFeed = async () => {
    await axios.post(
        `${process.env.REACT_APP_API}/news-feed/${page}`,
    )
    .then((res) => {
        setPosts(res.data);
        setPage(page + 1);
    })
    .catch((err) => {
        console.log(err);
    });
};

router.post('/news-feed/:page', newsFeed);

exports.newsFeed = async (req, res) => {
    try {
        const user = await User.findById(req.body.user._id);
        let following = user.following;
        following.push(req.body.user._id);

        const currentPage = req.params.page || 1;
        const perPage = 5;

        const posts = await Post.find({ postedBy: { $in: following } })
        .skip((currentPage - 1) * perPage)
        .sort({createdAt: -1})
        .limit(perPage);
        res.json(posts);
    } catch (err) {
        console.log(err);
    }
};

I want to use react-infinite-scroll-component to fetch only the first 20 posts on page load, then another 20 when user scrolls to the bottom etc.

I currently have this code to render the posts:

<div id='scrollableDiv' style={{ overflow: 'auto' }}>
    <InfiniteScroll
      dataLength={20}
      next={newsFeed}
      hasMore={true}
      loader={<FontAwesomeIcon icon={faSpinner} spin />}
      endMessage={
        <p style={{ textAlign: 'center' }}>
          <b>Yay! You have seen it all</b>
        </p>
      }
      scrollableTarget='scrollableDiv'
    >
        {posts.map((post) => (
        ...)}
    </InfiniteScroll>
</div>

What is now happening is that only the first 20 posts are rendered, with the loading spinner continuously displayed below.

I have tried adding page as a dependency to the useEffect callback and this does result in loading the posts in blocks of 20, however they are rendered automatically each second and overlap the previous block as if the app is literally changing the page and when all have been rendered an empty screen is displayed.


Solution

  • Your issue appears to be that your backend is returning all records every time you call it, since there are no parameters being passed on to it:

    await axios.post(
        `${process.env.REACT_APP_API}/news-feed`,
    )
    

    This is not consistent with the documentation of the component which states:

    The data is passed as children to the InfiniteScroll component and the data should contain previous items too. e.g. Initial data = [1, 2, 3] and then next load of data should be [1, 2, 3, 4, 5, 6].

    So, you need to tell your backend which set of items to return, then append those items to the posts you already loaded in prior calls.

    For instance, keep a reference of the page you need returned and call the backend with a parameter:

    //In state declaration
    const [pageNumber, setPageNumber] = useState(1);
    
    //In loader function
    await axios.post(
        `${process.env.REACT_APP_API}/news-feed/{pageNumber}`,
    ) 
    

    Where pageNumber is a variable you keep in state with the current page number, then increment it every time you receive items. Something along the lines of:

    await axios.post(
        `${process.env.REACT_APP_API}/news-feed/{pageNumber}`,
    )
    .then((res) => {
        let newPosts = posts;
        newPosts.concat(res.data);
        setPosts(newPosts);
        setPageNumber(pageNumber + 1);
    })
    

    If you are unable to modify the backend to support paging, then you will have to retrieve all records every time and slice them to select the ones you need to append. Not terribly difficult but very inefficient. Something like this:

    //In state declaration
    const [pageNumber, setPageNumber] = useState(0); //0 based
    
    //In data loader
    await axios.post(
        `${process.env.REACT_APP_API}/news-feed`,
    )
    .then((res) => {
        let newPosts = posts;
        let start = (pageNumber === 0) ? 0 : (pageNumber * 20) - 1;
        let addPosts = res.data.slice(start, 20);
        newPosts = newPosts.concat(addPosts);
        setPosts(newPosts);
        setPageNumber(pageNumber + 1);
    })
    

    I wrote this in a hurry so make sure to check for consistency and end of records. You get the idea. :-)