WittCode💻

React Infinite Scroll

By

Learn how to create an infinite scroll with React and ExpressJS using the intersection observer API and a custom useInfiniteScroll hook.

Table of Contents 📖

What is an Infinite Scroll?

An infinite scroll is a design that loads more content as the user scrolls through the page. It is typically used in applications that consume large amounts of data such as Facebook, Instagram, etc. For example, the application in the Gif below starts loading new data when the final element of the list comes into view.

Image

If we loaded all the data at once, it would cause an initially slow application and hence a bad user experience. This Gif is what we will build in this article.

useInfiniteScroll Hook

We will house all our infinite scroll logic inside a custom hook called useInfiniteScroll. This application will manage 3 states and one reference.

export default function useInfiniteScroll() {
  const [data, setData] = useState([]);
  const [isLoading, setIsLoading] = useState(false);
  const [index, setIndex] = useState(0);
  const spinnerRef = useRef(null);
}
  • data - the data we are loading from an API call
  • isLoading - used to show a spinner while the API call is still ongoing
  • index - the current set of data we are fetching. For example, it will be used to fetch data elements 1-5, then 6-10, etc.
  • spinnerRef - reference to the spinner that is displayed while waiting for data to load

Creating a Simple API

The API we will be fetching data from is one we will create on our own.

const blogs = [
  {title: 'Blog Index 1', description: 'This is a really interesting blog. You will learn a ton.', image: ''},
  {title: 'Blog Index 2', description: 'This is a really interesting blog. You will learn a ton.', image: ''},
  {title: 'Blog Index 3', description: 'This is a really interesting blog. You will learn a ton.', image: ''},
  {title: 'Blog Index 4', description: 'This is a really interesting blog. You will learn a ton.', image: ''},
  {title: 'Blog Index 5', description: 'This is a really interesting blog. You will learn a ton.', image: ''},
  {title: 'Blog Index 6', description: 'This is a really interesting blog. You will learn a ton.', image: ''},
  {title: 'Blog Index 7', description: 'This is a really interesting blog. You will learn a ton.', image: ''},
  {title: 'Blog Index 8', description: 'This is a really interesting blog. You will learn a ton.', image: ''},
  {title: 'Blog Index 9', description: 'This is a really interesting blog. You will learn a ton.', image: ''},
  {title: 'Blog Index 10', description: 'This is a really interesting blog. You will learn a ton.', image: ''},
  {title: 'Blog Index 11', description: 'This is a really interesting blog. You will learn a ton.', image: ''},
  {title: 'Blog Index 12', description: 'This is a really interesting blog. You will learn a ton.', image: ''},
  {title: 'Blog Index 13', description: 'This is a really interesting blog. You will learn a ton.', image: ''},
  {title: 'Blog Index 14', description: 'This is a really interesting blog. You will learn a ton.', image: ''},
  {title: 'Blog Index 15', description: 'This is a really interesting blog. You will learn a ton.', image: ''},
  {title: 'Blog Index 16', description: 'This is a really interesting blog. You will learn a ton.', image: ''},
  {title: 'Blog Index 17', description: 'This is a really interesting blog. You will learn a ton.', image: ''},
  {title: 'Blog Index 18', description: 'This is a really interesting blog. You will learn a ton.', image: ''},
  {title: 'Blog Index 19', description: 'This is a really interesting blog. You will learn a ton.', image: ''},
  {title: 'Blog Index 20', description: 'This is a really interesting blog. You will learn a ton.', image: ''},
  {title: 'Blog Index 21', description: 'This is a really interesting blog. You will learn a ton.', image: ''},
  {title: 'Blog Index 22', description: 'This is a really interesting blog. You will learn a ton.', image: ''},
  {title: 'Blog Index 23', description: 'This is a really interesting blog. You will learn a ton.', image: ''},
  {title: 'Blog Index 24', description: 'This is a really interesting blog. You will learn a ton.', image: ''},
  {title: 'Blog Index 25', description: 'This is a really interesting blog. You will learn a ton.', image: ''}
  ];
  
const INCREMENT = 5;

app.get('/api/blogs', async (req, res) => {
  // Get the desired blogs
  const {index} = req.query;
  const rangeMin = index * INCREMENT;
  const rangeMax = rangeMin + INCREMENT;
  const blogsToReturn = blogs.filter((blog, i) => (i >= rangeMin && i < rangeMax));
  // Wait 3 seconds to return new blogs
  await new Promise(res => {
    setTimeout(() => {
    res();
    }, 3000);
  });

  return res.status(200).json(blogsToReturn);
});
  • Contacted by a GET request to /api/blogs
  • Get the index from the query parameters, use it to get the desired range of data
  • Wait 3 seconds before sending these blogs back to the client. This is so we can better show the spinner on the client.

Fetching Data

We will fetch the data from this API inside a memoized function called fetchData.

const fetchData = useCallback(() => {
  setIsLoading(true);

  fetch(`http://localhost:1234/api/blogs?index=${index}`)
    .then(resp => resp.json())
    .then(data => {
      if (data?.length !== 0) {
        setData((prevItems) => [...prevItems, ...data]);
        setIndex((prevIndex) => prevIndex + 1);
      }
    })
    .catch((err) => {
      console.error(err);
    })
    .finally(() => {
      setIsLoading(false);
    });

}, [isLoading, index]);

The reason why this function is memoized with the useCallback hook will be explained soon but for now this is what this function does.

  • Set the isLoading state to true so the spinner is being displayed
  • Make an API to the route we created providing the index state
  • If data is returned then append the data to the data state and increment the index. This is because now we want the next range of data.
  • Catch any errors that may occur and then no matter success or error, hide the spinner by setting the isLoading state to false
  • Finally, we change the reference to the fetchData function when either the isLoading or index state variables change, more on this later

Intersection Observer API

Next, we will use the intersection observer API inside a useEffect hook to determine when to call this fetchData function.

useEffect(() => {
  const observer = new IntersectionObserver((entries) => {
    const spinner = entries[0];
    if (spinner.isIntersecting && !isLoading) {
      fetchData();
    }
  });

  // Observe the spinner element
  if (spinnerRef.current) {
    observer.observe(spinnerRef.current);
  }

  // When we unmount, no longer observe the loader
  return () => {
    if (spinnerRef.current) {
      observer.unobserve(spinnerRef.current);
    }
  };
}, [fetchData]);

Intersection observer is a browser API that executes a function when a specific element comes into view. We will use this API to call our fetchData function whenever our spinner referenced by useRef is scrolled into view.

  • Observe the spinner element referenced by useRef
  • The intersection observer is only observing the spinner so we know it is the first element. If the spinner is completely in view of the viewport and we are not loading data still, then fetch more data.
  • When the component unmounts, stop observing the spinner.

Finally, we just need to return the necessary state variables and reference from this custom hook.

return {data, isLoading, spinnerRef};

Displaying the Infinite Scroll

Now lets display the information from our API and create our spinner.

import Stack from '@mui/material/Stack';
import useInfiniteScroll from '../hooks/useInfiniteScroll';
import BlogCard from './BlogCard';
import Spinner from './Spinner';

const App = () => {
  const {data, isLoading, spinnerRef} = useInfiniteScroll();

  return (
    <Stack>
      {data.map((item, i) => (
        <BlogCard blog={item} key={i}/>
      ))}
      {/* Div that is observed by the observer API. When in view more data is fetched  */}
      <div ref={spinnerRef}>{isLoading ? <Spinner /> : null}</div>
    </Stack>
  );
};

export default App;
  • Import our state and reference from the useInfiniteScroll hook
  • Loop through our data and display each in a component called BlogCard
  • Create a spinner at the bottom of the list. That way when we scroll to the bottom of the list the spinner will come into view and more data will be loaded.

Note that it is a better idea to load this data before the user gets to the bottom of the list. It was only placed at the bottom here as it is the easiest way to demonstrate the infinite scroll in action.