Implementing infinite scroll in React with Apollo Client
Asked Answered
C

0

7

In my NextJS app, I have a PostList.jsx component that looks like this:

import { useQuery } from '@apollo/react-hooks';
import Typography from '@material-ui/core/Typography';
import { NetworkStatus } from 'apollo-client';
import gql from 'graphql-tag';
import getPostsQuery from '../../apollo/schemas/getPostsQuery.graphql';
import Loading from './Loading';
import Grid from '@material-ui/core/Grid';
import PostPreview from './PostPreview';
import withStyles from '@material-ui/core/styles/withStyles';
import React, { useLayoutEffect } from 'react';

const styles = (theme) => ({
  root: {
    padding: theme.spacing(6, 2),
    width: '100%',
  },
});

export const GET_POSTS = gql`${getPostsQuery}`;

export const getPostsQueryVars = {
  start: 0,
  limit: 7,
};

const PostsList = (props) => {
  const { classes } = props;
  const {
    loading,
    error,
    data,
    fetchMore,
    networkStatus,
  } = useQuery(
    GET_POSTS,
    {
      variables: getPostsQueryVars,
      // Setting this value to true will make the component rerender when
      // the "networkStatus" changes, so we'd know if it is fetching
      // more data
      notifyOnNetworkStatusChange: true,
    },
  );

  const loadingMorePosts = networkStatus === NetworkStatus.fetchMore;

  const loadMorePosts = () => {
    fetchMore({
      variables: {
        skip: posts.length
      },
      updateQuery: (previousResult, { fetchMoreResult }) => {
        if (!fetchMoreResult) {
          return previousResult
        }
        return Object.assign({}, previousResult, {
          // Append the new posts results to the old one
          posts: [...previousResult.posts, ...fetchMoreResult.posts]
        })
      }
    })
  };

  const scrollFunction = () => {
    const postsContainer = document.getElementById('posts-container');
    if (postsContainer.getBoundingClientRect().bottom <= window.innerHeight) {
      console.log('container bottom reached');
    }
  };

  useLayoutEffect(() => {
    document.addEventListener('scroll', scrollFunction);
    scrollFunction();
    // returned function will be called on component unmount
    return () => {
      document.removeEventListener('scroll', scrollFunction);
    };
  }, []);

  if (error) return <div>There was an error!</div>;
  if (loading) return <Loading />;

  const { posts, postsConnection } = data;
  const areMorePosts = posts.length < postsConnection.aggregate.count;

  return (
    <Grid item className={classes.root}>
      <Grid container spacing={2} direction="row" id="posts-container">
        {posts.map((post) => {
          return (
            <Grid item xs={12} sm={6} md={4} lg={3} xl={2} className={`post-preview-container`}>
              <PostPreview
                title={post.title}
                excerpt={post.excerpt}
                thumbnail={`https://i.schandillia.com/d/${post.thumbnail.hash}${post.thumbnail.ext}`}
              />
            </Grid>
          );
        })}
      </Grid>
      {areMorePosts && (
        <button onClick={() => loadMorePosts()} disabled={loadingMorePosts}>
          {loadingMorePosts ? 'Loading...' : 'Show More'}
        </button>
      )}
    </Grid>
  );
};

export default withStyles(styles)(PostsList);

As you can see, this component fetches documents from a database via a GraphQL query using Apollo Client and displays them paginated. The pagination is defined by the getPostsQueryVars object. Here, if you scroll down to the bottom and there still are posts available, you'll get a button clicking which the next set of posts will be loaded.

What I'm keen on doing here is implement some kind of an infinite scroll and do away with the button altogether. So far, I've added a scroll event function to the component using React hooks and can confirm it's triggering as expected:

const scrollFunction = () => {
    const postsContainer = document.getElementById('posts-container');
    if (postsContainer.getBoundingClientRect().bottom <= window.innerHeight) {
      console.log('container bottom reached');
    }
  };

  useLayoutEffect(() => {
    document.addEventListener('scroll', scrollFunction);
    scrollFunction();
    return () => {
      document.removeEventListener('scroll', scrollFunction);
    };
  }, []);

But how do I proceed from here? How do achieve the following once the container bottom is reached AND areMorePosts is true:

  1. Display a <h4>Loading...</h4> right before the last </Grid>?
  2. Trigger the loadMorePosts() function?
  3. remove <h4>Loading...</h4> once loadMorePosts() has finished executing?
Chagall answered 24/9, 2019 at 6:45 Comment(1)
Hi, just wanted to know whether you have found a solution to this, because I am having the same issue with my listing?Balladry

© 2022 - 2024 — McMap. All rights reserved.