How to Create an Infinite Scroll Component in React

How to Create an Infinite Scroll Component in React

With the help of the Intersection Observer API

Ferenc Almasi • 2023 January 10 • 📖 12 min read

Infinite scroll is a popular pagination technique that works by automatically loading new pages with scrolling. No interaction is needed, and content is automatically populated once the user reaches the end of the page.

In this tutorial, we are going to take a look at how to build an infinite scroll component in React using the IntersectionObserver API. You will also learn about other concepts, such as render props, and using refs. The full project is also hosted on GitHub.

Looking to improve your skills? Check out our interactive course to master JavaScript from start to finish.
Master JavaScript

Creating an InfiniteScroll Component in React

First things first, let's outline how we want our component to look. We want to make things configurable so that it can be reused elsewhere within the same application. Based on this, our component will look like the following at the end of this tutorial:

import InfiniteScroll from './InfiniteScroll'

function App() {

    return (
        <InfiniteScroll
            url="https://dummyjson.com/posts"
            limit={50}
            render={posts => posts.map((item, index) => (
                <article key={index}>
                    #{item.id}: {item.title}
                </article>
            ))}
        >
            <div className="loader">Loading...</div>
        </InfiniteScroll>
    )
}
App.jsx Outline of the InfiniteScroll component
Copied to clipboard!

We want our component to accept three different props that can work together. These are:

  • url: The endpoint we want to use for fetching data. In this tutorial, we are using dummyjson.com, a service for generating fake JSON data for development and testing.
  • limit: The amount of items we want to display for each page.
  • render: This prop will accept a callback function that has access to the data that is loaded through the provided URL. This is what will be rendered for each item.

The above technique is called render props. It's a common way to dynamically handle render logic instead of implementing the InfiniteScroll component own render. Whatever is passed to the render prop will be rendered for each item.

We also have the option to add children to our component. In case the resource is loading, this is what we are going to display in place of the render prop. Now let's take a look at how our component is structured.


Loading the First Page with the Fetch API

As the very first thing, we want to define some state for our component. We are going to make use of the following useState variables in order to grab the first page:

import React, { useEffect, useState } from 'react'

const InfiniteScroll = ({
    url,
    limit,
    render,
    children
}) => {
    const [data, setData] = useState(null)
    const [loading, setLoading] = useState(true)
    const [page, setPage] = useState(1)
    const [endpoint, setEndpoint] = useState(`${url}?limit=${limit || 50}`)

    return (...)
}

export default InfiniteScroll
InfiniteScroll.jsx Notice that props are destructured to be ready to used right away
Copied to clipboard!
  • data: The actual data that we will receive from the passed URL.
  • loading: A state to tell whether the data is already returned or not. Since we want to grab the first page as soon as the component loads, this will be set to true initially.
  • page: The current page that we are on, which will be 1 by default.
  • endpoint: The final URL we are going to use to make a request. We can make the limit prop optional by falling back to 50 as a hardcoded value using a logical OR.

DummyJSON uses the limit and skip query params for pagination. You will need to update the URL with your own params to use it with your own API.

Based on the hooks, we can use the data variable with the render prop to render the content passed to the render prop:

return (
    <React.Fragment>
        {data && render(data)}
        {children}
    </React.Fragment>
)
InfiniteScroll.jsx How to render a render prop
Copied to clipboard!

However, since our data is currently null, this will only render the children, which shows a loading text. To grab the first batch of content, we want to use fetch on the url prop. To do this, we can extend our component with the following functions:

...
const [endpoint, setEndpoint] = useState(`${url}?limit=${limit || 50}`)

// A reusable function for generating the final URL for subsequent pages
const getUrl = (url, page, limit) => {
    return `${url}?limit=${limit || 50}&skip=${page * limit || 50}`
}

const setInitialData = async () => {
    const response = await fetch(endpoint)
    const json = await response.json()
    
    setData(json[Object.keys(json)[0]])
    setPage(page + 1)
    setEndpoint(getUrl(url, page, limit))
    setLoading(false)
}

useEffect(() => {
    if (!data) {
        setInitialData()
    }
}, [loading, page, endpoint])

return (...)
InfiniteScroll.jsx Loading the first page
Copied to clipboard!

Alternatively, we could also use a custom useFetch hook in place of fetch. Based on the state of our url, page and limit variables, we can create a function called getUrl for generating the correct URL for each page. We can use this function inside the setInitialData function which is responsible for grabbing the first page. We can call this function inside a useEffect if our data is still null.

Notice that useEffect has a dependency on loading, page and endpoint.

Understanding the setInitialData function

To better understand how this piece of code works, let's break down the setInitialData function with some comments. In order, we want to do the following:

// Set the function `async` to use `await`
const setInitialData = async () => {

    // Request the data from the endpoint, and return it in a JSON format
    const response = await fetch(endpoint)
    const json = await response.json()
    
    // Set the data to the first property of the `json` variable
    setData(json[Object.keys(json)[0]])
   
    // Increase our page number by 1
    setPage(page + 1)
    
    // Update the url to point to the next page
    setEndpoint(getUrl(url, page, limit))
    
    // Tell the component we loaded everything
    setLoading(false)
}
InfiniteScroll.jsx How the setInitialData function works
Copied to clipboard!

There are two main things that we need to point out. Since the actual data of the response (the posts that we will display) will always be the first property of the JSON object, we can grab it by using Object.keys:

const json = {
    posts: [1, 2, 3],
    limit: 50
}

Object.keys(json)
<- ['posts', 'limit']

Object.keys(json)[0]
<- 'posts'

json[Object.keys(json)[0]] // Also equivalent to `json.posts`
<- [1, 2, 3]
Grabbing the first property of an object using Object.keys
Copied to clipboard!

Next, the getUrl function will also add a skip query parameter to the end of the URL. We can multiply the current page with the limit to get how many results should be skipped. This means we will end up with the following URLs:

https://dummyjson.com/posts?limit=50          # Initial page
https://dummyjson.com/posts?limit=50&skip=50  # 2nd page
https://dummyjson.com/posts?limit=50&skip=100 # 3rd page
How URLs are structured
Copied to clipboard!
Looking to improve your skills? Check out our interactive course to master JavaScript from start to finish.
Master JavaScript

Loading Subsequent Pages with Intersection Observer

The only thing left to do is to add the intersection observer to load subsequent pages after the first page is loaded. To do this, we want to observe the children of the component (the loading text). If it is visible — meaning the user scrolled down to the end of the page — we want to load the next page. For this, we are going to need a reference to the element. This can be done by using useRef with React.cloneElement:

- import React, { useEffect, useState } from 'react'
+ import React, { useEffect, useRef, useState } from 'react'

const InfiniteScroll = ({
    url,
    limit,
    render,
    children
}) => {
+   const element = useRef(null)
    const [data, setData] = useState(null)
    const [loading, setLoading] = useState(true)
    const [page, setPage] = useState(1)
+   const [isLastPage, setIsLastPage] = useState(false)
    const [endpoint, setEndpoint] = useState(`${url}?limit=${limit || 50}`)
+   const childWithRef = React.cloneElement(children, { ref: element })

...

return (
    <React.Fragment>
        {data && render(data)}
-       {children}
+       {childWithRef}
    </React.Fragment>
)
InfiniteScroll.jsx Don't forget to also add useRef to the import
Copied to clipboard!

If you are passing a component as a child to InfiniteScroll, you will need to use forwardRef to pass the correct reference.

This way, now we are going to be able to access the DOM element directly from the component, without having to pass a ref to children. It's also time to add a new state to check if we are on the last page. We can use this to only observe the element if there are still pages. To observe this element, we want to add the following to our useEffect:

const intersectionObserver = new IntersectionObserver(async entries => {
    const entry = entries[0]
    
    if (entry.isIntersecting && !loading) {
        // TODO - request the new page
    }
})

useEffect(() => {
    if (!data) {
        setInitialData()
    }

    if (!isLastPage) {
        intersectionObserver.observe(element.current)
    }

    return () => {
        element.current && intersectionObserver.unobserve(element.current)
    }
}, [loading, page, endpoint])

return (
    <React.Fragment>
        {data && render(data)}
        (/* Only show the loading if we are not on the last page */)
        {!isLastPage && childWithRef}
    </React.Fragment>
)
InfiniteScroll.jsx Creating the IntersectionObserver
Copied to clipboard!

To create a new intersection observer, we can call IntersectionObserver with the new keyword, passing a callback function. In our case, as we are going to deal with network requests, we can make the callback async. This function has access to the list of entries that are intersecting with the viewport.

This will always return an array of elements, but since we are observing a single element, we can safely grab the first index. Each entry has an isIntersecting property that will be true whenever the element becomes visible in the viewport. Based on this and the loading state, we can fire requests here for subsequent pages.

To start observing the element, we just need to call observe on the instance of the IntersectionObserver. We need to pass the element we want to observe, in our case, the loading indicator that we can access using element.current (created by the useRef hook).

Notice that the useEffect hook also accepts a return function that will be called whenever the component is unmounted. In this case, we want to call unobserve to stop observing the element. Now let's see what goes inside the if statement exactly.


Requesting next pages

setLoading(true)
setPage(page + 1)
setEndpoint(getUrl(url, page, limit))

const response = await fetch(endpoint)
const json = await response.json()

setData([
    ...data,
    ...json[Object.keys(json)[0]]
])

setLoading(false)

if (json.total === json.skip + json.limit) {
    setIsLastPage(true)
    intersectionObserver.unobserve(element.current)
}
InfiniteScroll.jsx
Copied to clipboard!

We want to start off by updating the state of the component: setting loading to true, increasing our page by 1, and updating the URL. Here we are ready to do the same fetch we did inside the setInitialData function. Once the response is back, we can expand the data with the new dataset using the spread operator. This is essentially merging two arrays together. From here on, we can disable loading, and also add a logic for checking if we are on the last page.

DummyJSON provides a total, skip and limit properties to each response. If the sum of skip and limit equals to the total variable (which represents the total number of items across all pages), then we are at the very last page. This is the time when we want to remove the observer and hide the loading via the isLastPage state.

Looking to improve your skills? Check out our interactive course to master JavaScript from start to finish.
Master JavaScript

Summary

And now you have a working example of an infinite scrolling component in React! As mentioned at the beginning of this tutorial, you can also find the project hosted on GitHub.

There are also many popular libraries out there such as react-infinite-scroll-component or react-infinite-scroller that can be used for infinite scrolling, however, keep in mind when using third-party packages, that you will likely end up with more code than you actually need. Thank you for reading through, happy coding! 👨‍💻

Share on
  • twitter
  • facebook
JavaScript Course Dashboard

Tired of looking for tutorials?

You are not alone. Webtips has more than 400 tutorials which would take roughly 75 hours to read.

Check out our interactive course to master JavaScript in less time.

Learn More

Recommended