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 to get some dummy posts.
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.
// 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
.
// 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.
// 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
.
// 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 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!
// 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.
Demonstration
We did it! Now we have an infinite scroll in our posts list.
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.
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!
Top comments (0)