GraphQL Infinite Scroll
Tags: react, graphql, infinitescroll, webdev
Published: 2023-11-26 18:08:23

Ever wonder how to enhance your website with an infinite scroll feature that keeps your users endlessly engaged?

In this article, we’re breaking down the process of creating an **infinite scroll using GraphQL and React**. By the end of this guide, you’ll be able to implement it in your own web projects. 

Let’s dive in and unlock the secrets of infinite scrolling with GraphQL and React—it’s easier than you might think!

---

## The Posts List

We are implementing a simple post list to serve as an example for this article. We will use this [api](https://graphqlzero.almansi.me/) to get some dummy posts.


![Main Layout](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/a701ndp3ckuhmxutxn7i.png)

In our case, we want to show the top 100 most rated post. Initially, we don't want to load all the posts at once. It would be more appropiate to load some posts, and when the user scrolls down the least then we load some more, and so on. That's the purpose of infinite scrolling: **saving users from an initial full page load**.

So, here we have the code that renders the page where we show all the posts.


```typescript
// src/pages/posts/index.tsx
import Spinner from '~/components/Spinner'

import useLayout from './hooks'
import Post from './Post'
import {
  Container,
  Content,
  Description,
  Header,
  List,
  PageTitle,
  Title,
} from './styles'

const Layout = () => {
  const { loading, posts, thresholdElementRef } = useLayout()

  return (
    <Container>
      <PageTitle>Infinite Scroll</PageTitle>
      <Content>
        <Header>
          <Title>100 Most rated posts</Title>
          <Description>Check them all by scrolling down!</Description>
        </Header>
        {loading ? (
          <Spinner />
        ) : (
          <List>
            {posts.map(({ id, title }, index) => (
              <Post
                id={id}
                key={id}
                ref={
                  index === posts.length - 1 ? thresholdElementRef : undefined
                }
                title={title}
              />
            ))}
          </List>
        )}
      </Content>
    </Container>
  )
}

export default Layout
```

We are using a **custom hook** `useLayout`, from where we are extracting three things:

- `loading`: Not relevant to this article. Just tells us if the query is still loading.
- `posts`: **The posts that are being rendered**.
- `thresholdElementRef`: **A reference attached to the element that will serve as a threshold**. In our case, that element will be the last post.

Let's take a look at our `useLayout`.

```typescript
// src/pages/posts/hooks.tsx
import usePosts from '~/hooks/usePosts'
import useInfiniteScroll from '~/lib/use-infinite-scroll'

const useLayout = () => {
  const { fetchMorePosts, loading, posts } = usePosts()

  const { thresholdElementRef } = useInfiniteScroll({
    fetchNextPage: fetchMorePosts,
    options: { rootMargin: '400px' },
  })

  return { loading, posts, thresholdElementRef }
}

export default useLayout

```

Right here, we are creating the `thresholdElementRef` using 
`useInfiniteScroll`. This library receives the `fetchMorePosts` function and some options as props. Let's break everything down step by step.

---

## The `useInfiniteScroll` lib

This library will allow us to obtain a threshold reference, which we will asign to the element that will serve as a threshold.

```typescript
// src/lib/use-infinite-scroll/index.ts
import useIntersectedElement from '../use-intersected-element'
import { UseInfiniteScrollProps } from './types'

const useInfiniteScroll = <ThresholdElement extends Element = Element>({
  fetchNextPage,
  options,
}: UseInfiniteScrollProps) => {
  const { thresholdElementRef } = useIntersectedElement<ThresholdElement>({
    callback: fetchNextPage,
    options,
  })

  return { thresholdElementRef }
}

export default useInfiniteScroll
```

This library just passes the received props to another library: `useIntersectedElement`.

```typescript
// src/lib/use-intersected-element/index.ts
import { useEffect, useMemo, useState } from 'react'

import { UseIntersectedElementProps } from './types'

const useIntersectedElement = <ThresholdElement extends Element = Element>({
  callback,
  options,
}: UseIntersectedElementProps) => {
  const [thresholdElement, thresholdElementRef] =
    useState<ThresholdElement | null>(null)

  const observer = useMemo(
    () =>
      new IntersectionObserver(([entry]) => {
        if (!entry.isIntersecting) return

        callback()
      }, options),
    [callback, options],
  )

  useEffect(() => {
    if (!thresholdElement) return

    observer.observe(thresholdElement)

    return () => {
      observer.unobserve(thresholdElement)
    }
  }, [observer, thresholdElement])

  return { thresholdElementRef }
}

export default useIntersectedElement

export type { UseIntersectedElementProps }
```

Here, we create a state, whose _setter_ is the ref that we've been talking about. Remember that, ultimately, **this threshold element will be the last post of the list**, as we mentioned before.

Then, we use `IntersectionObserver` to create an observer, which will execute the `callback` function received as an argument (**remember the** `fetchMorePosts` **function in the** `useLayout` **hook?**). If the `entry` (the target element) is not been intersected, then we execute nothing.

It can also receive some options. In our case (**remember the** `useLayout` **hook**), we only use the `rootMargin: '400px'`, which increase the size of the root element's bounding box before computing intersections. You can take a look at the [documentation](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) in case of any doubts.

Finally, in the `useEffect` we are observing the threshold element, and unobserving it when the component unmounts.

---

## The `fetchMorePosts`

**What does GraphQL have to do with all of this?** Well, here it comes!

```typescript
// src/hooks/usePosts/index.ts
import { useQuery } from '@apollo/client'
import { useCallback, useMemo } from 'react'

import POSTS from '~/graphql/queries/posts'
import { PostsQuery, PostsQueryVariables } from '~/graphql/types'
import Post from '~/models/post'

import { MAX_NUMBER_OF_POSTS, POSTS_LIMIT } from './constants'

const usePosts = () => {
  const { data, fetchMore, loading } = useQuery<
    PostsQuery,
    PostsQueryVariables
  >(POSTS, {
    variables: {
      options: { paginate: { limit: POSTS_LIMIT, page: 1 } },
    },
  })

  const posts = useMemo(
    () => (data ? data.posts?.data?.map(Post.fromDto) ?? [] : []),
    [data],
  )

  const fetchMorePosts = useCallback(() => {
    if (posts.length === MAX_NUMBER_OF_POSTS) return

    fetchMore({
      updateQuery: (prev, { fetchMoreResult }) => {
        if (!fetchMoreResult) return prev

        return Object.assign({}, prev, {
          posts: {
            data: [
              ...(prev.posts?.data ?? []),
              ...(fetchMoreResult.posts?.data ?? []),
            ],
          },
        })
      },
      variables: {
        options: { paginate: { page: posts.length / POSTS_LIMIT + 1 } },
      },
    })
  }, [fetchMore, posts])

  return { fetchMorePosts, loading, posts }
}

export default usePosts
``` 

We need to use `useQuery` to fetch the data from the api. It is important that the api supports **pagination**. Pagination is a process used to **divide a large dataset into smaller chunks** (pages). This will allow us to request a "page" per request (ultimately allowing the implementation of an infinite scroll). In our case, we will request 10 posts per "page". We will store all those posts in `posts`.

So, here we have (finally 😅) the `fetchMorePosts` function. It makes use of the `fetchMore` function, which allows to send followup queries to our GraphQL server to obtain additional pages. 

The behaviour would be the following: **If we've reached the maximum number of posts, we stop requesting data. If not,** `fetchMorePosts` **will request the next 10 posts, adding them to** `posts`.

You can find more information about GraphQL pagination in the [official documentation](https://www.apollographql.com/docs/react/pagination/core-api/).

---

## Demonstration

We did it! Now we have an infinite scroll in our posts list.

![infinte-scroll](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l6zhkespspua0ivnc3eh.gif)

So, to sum up:

- We **set the threshold reference to the last post** that is being rendered (firstly, it will be the 10th, then the 20th, etc.)
- We set the callback `fetchMorePosts` to the threshold, and **execute it when the threshold is reached**.
- `fetchMorePosts` will query 10 posts at a time, **adding them to the ones that have already been requested**, until the limit is reached.

All the project code is available in this [Github repository](https://github.com/moruno21/infinite-scroll).

---

## Final thoughts

Deciding to use GraphQL shouldn't be about adding infinite scroll. It's a bigger decision. However, GraphQL is a versatile tool that allows us to apply and implement some great things, and infinite scroll is not an exception.

I hope you've enjoyed the article and that it has encouraged you to add it to your projects. Thanks for your time!