Infinite scrolling with React Query
Fetching and rendering large amounts of data at once could have negative effects like performance issues leading to a bad user experience. To effectively work with large data, two patterns are mainly used, pagination and infinite scrolling, both dealing with rendering or fetching small chunks of data at a time.
In this tutorial, we will learn how to implement infinite scrolling in React using React Query. To follow up with this tutorial, you should be familiar with React, React hooks, and React Query.
Introduction to Infinite scrolling
Infinite scrolling is a pattern used in apps to load content continuously as the user scrolls to the bottom of the page. It’s most useful for loading large data sets from a server in chunks to improve performance by reducing the time taken to fetch and render content. Social media sites like Facebook, Twitter, etc., use this to display feeds. Other sites with user-generated content, like YouTube, also implement this pattern.
Aside from infinite scrolling, there is another design pattern for loading large data sets in chunks: pagination. But unlike infinite scrolling with pagination, users must click a button to load the following content when they scroll to the bottom of the page. Both patterns have their use cases. Infinite scrolling is better suited for exploring content, where users are browsing aimlessly for something interesting, which is not the case for pagination.
Implementing infinite scrolling in React
In this tutorial, we will be using the GitHub Search API, which allows us to search for items on GitHub. The API can fetch many items in one request or small chunks depending on the parameters passed to it. We will use it to search for repositories on GitHub. Here is an example of the API URL to be used in this tutorial: https://api.github.com/search/repositories?q=topic:react&per_page=5&page=1. This will get the first page containing five React repositories on GitHub. (Note that this API call is a clear example of pagination!)
I have already created a starter repo where I have added the functionality to search and display React repositories on GitHub using React Query, so we can focus on how to implement infinite scrolling. Clone the repo with the following commands:
git clone -b starter https://github.com/mandah-israel/infinite-scrolling.git
cd infinite-scrolling
npm i
Now, when we start the app using $ npm start
, we will see the following screen:
React Query provides a hook for infinite scrolling, useInfiniteQuery
. It’s similar to the [useQuery](https://react-query.tanstack.com/reference/useQuery)
hook with a few differences:
- The returned data is now an object containing two array properties. One is
pageParams
access withdata.pageParams
, an array containing the page params used to fetch the pages. The other ispages
access withdata.pages
, an array containing the fetched pages. - The options passed as the third parameter include
getNextPageParam
andgetPreviousPageParam
to determine if there is more data to load. fetchNextPage
andfetchPreviousPage
functions are included as returned properties for fetching the next and previous pages, respectively.hasNextPage
andhasPreviousPage
boolean properties are returned to determine if there is a next or previous page.isFetchingNextPage
andisFetchingPreviousPage
boolean properties are returned to check when the next page or previous page is fetching.
We only need to use a few options/properties above to implement infinite scrolling. We will first replace the useQuery
hook with useInfiniteQuery
and also update the function used to fetch the data in our starter app. To do this, head over to the App.js
file and add the following import:
// App.js
import { useInfiniteQuery } from "react-query"
Next, in the App
component replace fetchRepositories
and useQuery
with the following lines of code:
// App.js`
function App() {
const LIMIT = 10
const fetchRepositories = async (page) => {
const response = await fetch(`https://api.github.com/search/repositories?q=topic:react&per_page=${LIMIT}&page=${page}`)
return response.json()
}
const {data, isSuccess, hasNextPage, fetchNextPage, isFetchingNextPage} = useInfiniteQuery(
'repos',
({pageParam = 1}) => fetchRepositories(pageParam),
{
getNextPageParam: (lastPage, allPages) => {
const nextPage = allPages.length + 1
return nextPage
}
}
)
...
In the above code, we have modified the API URL by adding page
and per_page
parameters, which will fetch only ten repositories per page. We have replaced the useQuery
hook with useInfiniteQuery
, making available the needed properties. We are also adding the getNextPageParam
option, which receives the last page of the infinite list of data and the array of all pages that have been fetched. This function either returns a value to be used to get the following data or undefined
indicating data is no longer available. We are returning the following page param in the function, which will be used when the fetchNextPage
function is called to get the next page.
Next, let’s modify how we are accessing and displaying our data. In the return
statement, modify the div
with the app
class name to look like this:
// App.js
<div className="app">
{isSuccess && data.pages.map(page =>
page.items.map((comment) => (
<div className='result' key={comment.id}>
<span>{comment.name}</span>
<p>{comment.description}</p>
</div>
))
)}
</div>
With this, the fetched results should now be displayed. For Infinite scrolling to start working, we need to call the fetchNextPage
function any time we scroll to the bottom of the page. To see when we get to the bottom of the page, we can do that using either the browser Scroll event or the Intersection Observer API. We will cover both of them in the following sections.
Open Source Session Replay
OpenReplay is an open-source, session replay suite that lets you see what users do on your web app, helping you troubleshoot issues faster. OpenReplay is self-hosted for full control over your data.
Start enjoying your debugging experience - start using OpenReplay for free.
Fetching new data using scroll events
In the App.js
file, first add the following import:
// App.js
import { useEffect } from 'react';
Next, add the following lines of code in the App
component after the useInfiniteQuery
hook:
// App.js
useEffect(() => {
let fetching = false;
const handleScroll = async (e) => {
const {scrollHeight, scrollTop, clientHeight} = e.target.scrollingElement;
if(!fetching && scrollHeight - scrollTop <= clientHeight * 1.2) {
fetching = true
if(hasNextPage) await fetchNextPage()
fetching = false
}
}
document.addEventListener('scroll', handleScroll)
return () => {
document.removeEventListener('scroll', handleScroll)
}
}, [fetchNextPage, hasNextPage])
In the above code, we added a scroll event listener to the document, which calls the handleScroll
function when fired. In it, we detect when we get to the bottom of the page using the scrollingEvent
property, then call fetchNextPage
to fetch the next page if hasNextPage
is true
.
Right now, hasNextPage
will always be true
because in the getNextPageParam
option, we are returning a param which is the value to get the next page. For hasNextPage
to be false
, we need to return undefined
or any other falsy value in getNextPageParam
. We will do this later to create the functionality to stop fetching data events after scrolling to the bottom.
With this, new data will be fetched when we start our app and scroll close to the bottom of the page.
Fetching new data using Intersection Observer
The Intersection Observer API provides a way to observe the visibility and position of a DOM element relative to the containing root element or viewport. Simply put, it monitors when an observed element is visible or reaches a predefined position and fires the callback function supplied to it. Using this API to implement the infinite scrolling functionality, we will first create an element at the bottom of our fetched data which will be the observed element. Then when this element is visible, we will call the fetchNextPage
function. Let’s do that.
First, add the following imports to the App.js
file:
import {useRef, useCallback} from 'react'
Next, add the following lines of code before the closing tag (</div>
) of the div
with the className
of app.
// App.js
<div className="app">
...
<div className='loader' ref={observerElem}>
{isFetchingNextPage && hasNextPage ? 'Loading...' : 'No search left'}
</div>
</div>
Above we created the div
element we want to observe using Intersection Observers. We have added the ref
attribute to access it directly. The above div
will display Loading…
or No search left
depending on isFetchingNextPage
and hasNextPage
boolean values.
Next, add the following line of code at the top of the App
component:
// App.js
function App() {
const observerElem = useRef(null)
...
Here we have created the observerElem
variable that was passed on to the ref
attribute. With this, when the DOM loads, we can access the div
element we created above. We are doing this to pass the div
element to the Intersection Observer from our code. Next, add the following lines of code after the useInfiniteQuery
hook.
// App.js
const handleObserver = useCallback((entries) => {
const [target] = entries
if(target.isIntersecting) {
fetchNextPage()
}
}, [fetchNextPage, hasNextPage])
useEffect(() => {
const element = observerElem.current
const option = { threshold: 0 }
const observer = new IntersectionObserver(handleObserver, option);
observer.observe(element)
return () => observer.unobserve(element)
}, [fetchNextPage, hasNextPage, handleObserver])
Above, we created a handleObserver
function which is the callback passed to IntersectionObserver
. It calls fetchNextPage
when the target element specified with observer.observe(element)
has entered the viewport.
With this, new data will be fetched when we scroll to the bottom of the page in our app.
Control fetching depending on available data
Right now, even when there is no data left to fetch, and we scroll to the bottom of the page in our app, the fetchNextPage
will still be called, sending requests to the API to get more data. To prevent this, we need to return a false value (undefined, 0 null, false) in getNextPageParam
when no data is left. This way, the hasNextPage
returned property will be equal to false
when there is no data left, then we will use it to control when the fetchNextPage
function is called.
To do this, modify the getNextPageParam
option of the useInfiniteQuery
hook to look like this:
// App.js
getNextPageParam: (lastPage, allPages) => {
const nextPage = allPages.length + 1
return lastPage.items.length !== 0 ? nextPage : undefined
}
Above, we are returning the following page param or undefined
based on whether data was returned in the last fetch made.
Now let’s modify the handleObserver
function to call fetchNextPage
only when hasNextPage
equals false.
Here is what it will look like:
// App.js
const handleObserver = useCallback((entries) => {
const [target] = entries
if(target.isIntersecting && hasNextPage) {
fetchNextPage()
}
}, [fetchNextPage, hasNextPage])
With this, we are done implementing the infinite scrolling functionality.
Conclusion
Infinite scrolling displays large sets of data in chunks, reducing the rendering time of fetched data. This tutorial taught us how to implement it with the scrolling events and the Intersection Observer API when using React Query.