React Infinite Scroll Tutorial: With and Without a Library

Muhammad Syakirurohman
Frontend Engineer
Published on
React Infinite Scroll Tutorial: With and Without a Library

Infinite scroll is a modern web & application design concept that loads content continuously as the user scrolling down the page. It changes the function of pagination.

Implementing infinite scroll is suitable if you have a lot of data to load and you don’t want users to click the page number to see more data. It boosts user experience in your application.

As a developer, we can implement infinite scroll in any application, including a react application. React infinite scroll can be implemented in 2 ways, by manual implementation without a library and by using an infinite scroll library.

In this post, I will show and explain to you how to implement infinite scroll in a react project, with and without a library. Both ways have their own advantages and drawbacks.

Before we jump to the tutorial, make sure that you already know to initialize a react app using create-react-app boilerplate. Because I won’t explain the basic react tutorial here. I assume you already understand that.

In this tutorial, we are going to use react functional components and hooks. We also using react-lab to host demo examples and this project architecture to manage the project files.

How to Implement Infinite Scroll Without Library

Implementing a react infinite scroll without a library is the best if you want to make your react project as lightweight as possible. It also best if you’re going to put some customization on it.

Personally, I will choose this method to implement an infinite scroll on my react app. I don’t think it has many codes and logics to write.

We only need some states, an event listener for scroll, an API calling service, and function to load data & put some logics.

Create A Component

Suppose that we will create a user list page that has infinite scroll implemented. So, we need a component to implement it like this.

import React, { useState } from 'react';
 
export default function InfiniteScrollNoLibrary() {
	const [userList, setUserList] = useState([]);
	const [page, setPage] = useState(1);
	const [loading, setLoading] = useState(false);
	const [noData, setNoData] = useState(false);
 
	return (
		<div>
			<div className='section'>
				{userList.map((user, i) => (
					<div className='box m-3 user' key={i}>
						<img src={user.avatar} alt={user.first_name} />
						<div className='user-details'>
							<strong>Email</strong>: {user.email}
							<br />
							<strong>First Name</strong>: {user.first_name}
							<br />
							<strong>Last Name</strong>: {user.last_name}
							<br />
						</div>
					</div>
				))}
				{loading ? <div className='text-center'>loading data ...</div> : ''}
				{noData ? <div className='text-center'>no data anymore ...</div> : ''}
			</div>
		</div>
	);
}

I don’t put business logic and event listener yet. Let me explain the states and markup first.

To manually implement infinite scroll we need at least 4 states:

  • userList to store an array of user data from API. The default is an empty array.
  • page to count what page of user list to load. This helps us to not loading and adding the same data to the list.
  • loading to give a loading state when calling the API.
  • noData to give a no data state and stop API calling when there is no data anymore.

As you can see in the code above, userList state will be looped using map in the JSX markup. A ”loading …” and “no data anymore …” text will also be added every time the loading and noData state has true value.

Create A Service for API Calling

Before I add some logics to component, I create a service for calling user data first.

Actually, you can directly call an API in a component without creating a service. But, I personally prefer to separate it from the component. You can read the reason on my react project structure article.

import axios from 'axios';
 
export default {
	getList: async function (page) {
		try {
			let url;
			if ((page != null) & (page > 1)) {
				url = 'https://reqres.in/api/users?per_page=2&page=' + page;
			} else {
				url = 'https://reqres.in/api/users?per_page=2';
			}
			const response = await axios.get(url);
			return response.data;
		} catch (error) {
			throw error;
		}
	}
};

The getList function above accepts a page parameter to dynamically change URL string based on page number inserted. For dummy data, I use resreq.in users API.

Adding Some Logic to Component

After creating a service, now we will use it in a component along with some logics. Have a look at the full component codes below. I will explain it after that.

import React, { useState, useEffect } from 'react';
import UserService from 'services/UserService';
 
export default function InfiniteScrollNoLibrary() {
	const [userList, setUserList] = useState([]);
	const [page, setPage] = useState(1);
	const [loading, setLoading] = useState(false);
	const [noData, setNoData] = useState(false);
 
	window.onscroll = () => {
		if (window.innerHeight + document.documentElement.scrollTop === document.documentElement.offsetHeight) {
			if (!noData) {
				loadUserList(page);
			}
		}
	};
 
	useEffect(() => {
		loadUserList(page);
	}, []);
 
	const loadUserList = (page) => {
		setLoading(true);
		setTimeout(() => {
			UserService.getList(page)
				.then((res) => {
					const newPage = page + 1;
					const newList = userList.concat(res.data);
					setUserList(newList);
					setPage(newPage);
					if (res.data.length === 0) setNoData(true);
				})
				.catch((err) => {
					console.log(err);
				})
				.finally(() => {
					setLoading(false);
				});
		}, 1500);
	};
 
	return (
		<div>
			<div className='section'>
				{userList.map((user, i) => (
					<div className='box m-3 user' key={i}>
						<img src={user.avatar} alt={user.first_name} />
						<div className='user-details'>
							<strong>Email</strong>: {user.email}
							<br />
							<strong>First Name</strong>: {user.first_name}
							<br />
							<strong>Last Name</strong>: {user.last_name}
							<br />
						</div>
					</div>
				))}
				{loading ? <div className='text-center'>loading data ...</div> : ''}
				{noData ? <div className='text-center'>no data anymore ...</div> : ''}
			</div>
		</div>
	);
}

First, we import UserService and useEffect hook to the component. We will be use them later in the API calling function.

The most important codes in the component above are on the line 11 - 17.

window.onscroll = () => {
	if (window.innerHeight + document.documentElement.scrollTop === document.documentElement.offsetHeight) {
		if (!noData) {
			loadUserList(page);
		}
	}
};

This is a function to listen when the user scrolls the page. Inside it, I put a logic: “If user scrolls to the bottom of the page, and noData state is false, then load the user list”.

When a user just lands to the page and not scrolled yet, we load the user list inside the useEffect hook. So, the user data still loaded.

useEffect(() => {
	loadUserList(page);
}, []);

Now, look into the loadUserList function.

const loadUserList = (page) => {
	setLoading(true);
	setTimeout(() => {
		UserService.getList(page)
			.then((res) => {
				const newList = userList.concat(res.data);
				setUserList(newList);
 
				const newPage = page + 1;
				setPage(newPage);
 
				if (res.data.length === 0) setNoData(true);
			})
			.catch((err) => {
				console.log(err);
			})
			.finally(() => {
				setLoading(false);
			});
	}, 1500);
};

First, we set the loading state to true to show “loading …” text when calling the API. I use setTimeout function here just to delay API calls so I can see the loading state. You don’t have to use it in your codes.

In line 4, I call the getList function in UserService and pass page to it. If the API request success, new user data from API will be added to the current user list (line 6 - 7).

We also need to set new page state for the next API calling when user scrolls again. You can see it on line 9 - 10.

Last, we create a condition to set noData State. If the API response is an empty array, it means that there is no more data to be loaded. So, we set the noData state to true.

If the API request returns an error, catch it in the catch section. In this example, I just console.log it. And in finally section set the loading state to be false again because the request is over.

That’s it. Now you can practice it on your own. To see the live demo for infinite scroll without library, you can click the link below.

Live Demo

How to Implement Infinite Scroll With react-infinite-scroller

If you don’t want to implement react infinite scroll manually, you can still implement it using a library. There are a lot of libraries for React infinite scroll implementation out there.

Using a library for infinite scroll is the best if you want shorter code to write, and some options to easily customize it. Most of react infinite scroll libraries have more options and features than manual implementation i show you before.

In this tutorial, I use react-infinite-scroller as it is simple and easy to implement. Without further ado, let’s see how to use it in your react project.

Install and Import react-infinite-scroller

First you should install react-infinite-scroller to your project using npm

npm i react-infinite-scroller

To use it in a component, just import it like this.

 import InfiniteScroll from 'react-infinite-scroller'

Using InfiniteScroll in a Component

Here is the full component codes. I explain it below.

import React, { useState } from 'react';
import InfiniteScroll from 'react-infinite-scroller';
import UserService from 'services/UserService';
 
export default function InfiniteScrollerWithReactInfiniteScroller() {
	const [userList, setUserList] = useState([]);
	const [hasMoreItems, setHasMoreItems] = useState(true);
 
	const loadUserList = (page) => {
		setTimeout(() => {
			UserService.getList(page)
				.then((res) => {
					const newList = userList.concat(res.data);
					setUserList(newList);
 
					if (res.data.length === 0) {
						setHasMoreItems(false);
					} else {
						setHasMoreItems(true);
					}
				})
				.catch((err) => {
					console.log(err);
				});
		}, 1500);
	};
 
	return (
		<div>
			<div className='section'>
				<InfiniteScroll
					threshold={0}
					pageStart={0}
					loadMore={loadUserList}
					hasMore={hasMoreItems}
					loader={<div className='text-center'>loading data ...</div>}>
					{userList.map((user, i) => (
						<div className='box m-3 user' key={i}>
							<img src={user.avatar} alt={user.first_name} />
							<div className='user-details'>
								<strong>Email</strong>: {user.email}
								<br />
								<strong>First Name</strong>: {user.first_name}
								<br />
								<strong>Last Name</strong>: {user.last_name}
								<br />
							</div>
						</div>
					))}
				</InfiniteScroll>
				{hasMoreItems ? '' : <div className='text-center'>no data anymore ...</div>}
			</div>
		</div>
	);
}

As you can see, we have fewer states and logics to write if we use a library. We only need userList state to store our user data, and hasMoreItems to be passed to <InfiniteScroll/>. The page and loading state will be handled by react-infinite-scroll.

In the loadUserList function, we use the same UserService that I used before in the manual implementation. When the API request success, we only have to set a new user list (line 14-15) and set hasMoreItems state (line 17-21).

Most of logic is handled by <InfiniteScroll/> that should wrap the userList looping.

<InfiniteScroll
	threshold={0}
	pageStart={0}
	loadMore={loadUserList}
	hasMore={hasMoreItems}
	loader={<div className='text-center'>loading data ...</div>}>
	{userList.map((user, i) => (
		<div className='box m-3 user' key={i}>
			<img src={user.avatar} alt={user.first_name} />
			<div className='user-details'>
				<strong>Email</strong>: {user.email}
				<br />
				<strong>First Name</strong>: {user.first_name}
				<br />
				<strong>Last Name</strong>: {user.last_name}
				<br />
			</div>
		</div>
	))}
</InfiniteScroll>

As you see, there is some attribute for InfiniteScroll i used above. Here is the explanation.

  • threshold is the distance between the bottom of the page and the bottom of the window’s viewport that triggers the loading of new list - Defaults to 250. But i set it to 0.
  • pageStart is the page number corresponding to the initial list, defaults to 0 which means that for the first loading, loadMore will be called with 1.
  • loadMore(pageToLoad) is called when the user scrolls down and we need to load a new list. The value should be a function. It will pass page number to the value.
  • hasMore is a boolean stating whether there are more items to be loaded. Event listeners are removed if false.
  • loader is loader element to be displayed while loading items - You can use InfiniteScroll.setDefaultLoader(loader); to set a default loader for all your InfiniteScroll components

To use more attributes, you can see the documentation here. And if you want see the live demo for infinite scroll with react-infinite-scoller, you can click the link below.

Live Demo

That’s all. I hope this useful for you.

Happy coding!