Today we’re going to dive deeper into React development and learn how to build an infinite scroll component in React using React Hooks. If you are just starting out with React hooks make sure to check out this guide. If not, Let’s get started!
What We’re Building
You may not have heard of an infinite scroller before, but if you’ve ever used Facebook, Instagram or Reddit, you’ve definitely used one.
An infinite scroller is a modern alternative to pagination.
Rather than wait for the user to click next page, new content is automatically loaded every time the user reaches the bottom of the page. Therefore, there’s a never-ending supply of content for the user to read through.
Setting Up our Infinite Scroll Component
If you have an existing React project that you want to add the infinite scroller functionality to, you can skip this section.
For everyone else, the easiest way to get started is to use Create React App to create a brand new React project. Check out this guide to creating your first React app.
Once you have the app up and running, strip out all of the unneccesary code from the App.js component, so it looks like this:
I’m using Bootstrap in this tutorial. If you want to add bootstrap to your new React project, run the following command in your terminal at your root project directory:
npm install –save bootstrap.
import React from 'react';
const App = () => (
<div className="container">
<div className="row">
<div className="col-6 justify-content-center my-5">
...
</div>
</div>
</div>
);
export default App;
Building the List Component
Infinite scroll works great for loading lists of things like photos, status updates, and table data.
In
Create a new file called List.js inside of the src directory, and fill it with the following code:
import React, { useState } from 'react';
const List = () => {
const [listItems, setListItems] = useState(Array.from(Array(30).keys(), n => n + 1));
return (
<>
<ul className="list-group mb-2">
{listItems.map(listItem => <li className="list-group-item">List Item {listItem}</li>)}
</ul>
</>
);
};
export default List;
We do a few things using the code above:
- Importing React and the new useState hook.
- Defining a new stateful functional component called List.
- Instantiating a new variable called listItems, and a new setter function called setListItems with the help of our new useState hook.
- Setting the initial value of listItems to an Array filled with 30 values from 1 to 30.
- Finally, returning some HTML which displays an unordered list, loops through listItems and displayed a <li> tag with the value inside for every item in the listItems array.
Phew, we’re doing a lot for such a small amount of code! If you haven’t used React Hooks before, check out our Simple Introduction to React Hooks.
We need to use our new List component Before we see it in action.
Jump back to App.js and import the List component at the top of the file. Then, declare it in the App.js return function.
import React from 'react';
import List from './List';
const App = () => (
<div className="container">
<div className="row">
<div className="col-6 justify-content-center my-5">
<List />
</div>
</div>
</div>
);
export default App;
Save your files, jump over to your app running in the browser and you should see a brand new list showing 30 items.
Detecting the Bottom of the Page
Great! We’ve got a React List component, but it doesn’t infinite scroll. So let’s change that next.
An infinite scroll is triggered when you scroll to the bottom of the page. Therefore, we need to detect that the webpage scrollbar is at the end of the page.
There are two parts to accomplishing that. First, we add an on scroll event listener to the Window object to call a function called handleScroll every time the window scrolls.
Add the following to the List.js component:
import React, { useState, useEffect } from 'react';
const List = () => {
...
useEffect(() => {
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
...
};
export default List;
In the code above, we import another Hook, the
Let’s look at useEffect a little closer.
useEffect(() => {
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
Notice how we use an empty array [] as the second parameter of
That tells the
The second part to detecting the bottom of the page is to check if the Window object’s inner height, plus the Document object’s scrollTop, is equal to the Document’s offsetHeight.
If it is, then we want to fetch more items to add to our List.
...
function handleScroll() {
if (window.innerHeight + document.documentElement.scrollTop !== document.documentElement.offsetHeight) return;
console.log('Fetch more list items!');
}
...
Let’s tie all those two steps together, so our List component will look like this:
import React, { useState, useEffect } from 'react';
const List = () => {
const [listItems, setListItems] = useState(Array.from(Array(30).keys(), n => n + 1));
useEffect(() => {
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
function handleScroll() {
if (window.innerHeight + document.documentElement.scrollTop !== document.documentElement.offsetHeight) return;
console.log('Fetch more list items!');
}
return (
<>
<ul className="list-group mb-2">
{listItems.map(listItem => <li className="list-group-item">List Item {listItem}</li>)}
</ul>
</>
);
};
export default List;
Handling the isFetching State
Now that we have the scroll detection working, the next step is to fetch more list items and append them to the end of the listItems array.
Let’s stop for a moment and think about our List component in terms of its state.
When we scroll to the bottom of the page, List has to know to fetch more list items. Therefore its state changes from not fetching to fetching.
As a result, we need to add another state variable to our List component to keep track of that state change. Let’s call it isFetching.
...
const List = () => {
const [listItems, setListItems] = useState(Array.from(Array(30).keys(), n => n + 1));
const [isFetching, setIsFetching] = useState(false);
...
};
We define a new state variable called
Now that we have this new state variable, let’s modify our handleScroll function so that it sets the state of isFetching to true whenever we scroll to the bottom of the page:
function handleScroll() {
if (window.innerHeight + document.documentElement.scrollTop !== document.documentElement.offsetHeight) return;
setIsFetching(true);
}
Fetching More List Items
Right now we’re not doing anything with our new isFetching state change. Let’s change that.
With React Hooks and the useEffect Hook, we can now listen out for changes in state. Therefore, why don’t we listen for when
Add another useEffect function that listens for a change to isFetching. We do that by putting isFetching inside the array that’s passed to the second parameter to useEffect.
useEffect(() => {
if (!isFetching) return;
fetchMoreListItems();
}, [isFetching]);
This effect will call every time isFetching changes state. That includes when it’s false. We don’t want that, so we add a conditional at the top which returns whenever isFetching is false.
Finally, let’s add the function to fetch more list items.
...
function fetchMoreListItems() {
setTimeout(() => {
setListItems(prevState => ([...prevState, ...Array.from(Array(20).keys(), n => n + prevState.length + 1)]));
setIsFetching(false);
}, 2000);
}
...
For demo purposes, I’ve wrapped the setter inside a timeout to simulate two seconds of loading time that you might experience from a real-world application.
Once two seconds elapses, we call setListItems, and add another 20 listItems to the array. Afterwards, we set
Last but not least, let’s add a label underneath the list to tell the user that we’re fetching more items when
...
return (
<>
<ul className="list-group mb-2">
{listItems.map(listItem => <li className="list-group-item">List Item {listItem}</li>)}
</ul>
{isFetching && 'Fetching more list items...'}
</>
);
...
Save your component, jump back to the app running in your browser and take it for a spin!
You can either stop here or continue reading to see how we can simplify our React infinite scroll component by writing a custom React Hook to handle the logic for us.
Simplifying Infinite Scroll with a Custom React Hook
Although our Infinite Scroll component is working great, it’s not the best implementation.
Imagine you’re building a real-world web app with multiple components that use infinite scroll. For example, a list of users, a list of user’s photos, and a
We’d have to include the infinite scroll logic inside each of those components. That’s bad, because:
- We’re not staying DRY (Don’t Repeat Yourself)
- We’re wasting time re-writing code that’s already been written
- We’re not making it easy on ourselves if we want to change the infinite scroll logic. We’d have to change it in multiple places.
But, what if we didn’t have to duplicate that code?
React Hooks are a new addition since React 16.8. Writing custom React Hooks allow us to write functions to share common stateful logic between multiple components.
In other words, we can pull out all of the infinite scroll logic and put it inside a custom React Hook called
The useInfiniteScroll React Hook
Create a new file called useInfiniteScroll.js. We’re going to define a new function called useInfiniteScroll inside of it, like so:
import { useState } from 'react';
const useInfiniteScroll = (callback) => {
const [isFetching, setIsFetching] = useState(false);
};
export default useInfiniteScroll;
Our custom React Hook takes one parameter, a function called
We want our Hook to call whatever function we pass into the callback parameter after it has detected that the scrollbar is at the bottom of the page.
Let’s remove all of the logic we wrote previously in List.js, and put it inside the new
import { useState, useEffect } from 'react';
const useInfiniteScroll = (callback) => {
const [isFetching, setIsFetching] = useState(false);
useEffect(() => {
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
useEffect(() => {
if (!isFetching) return;
callback(() => {
console.log('called back');
});
}, [isFetching]);
function handleScroll() {
if (window.innerHeight + document.documentElement.scrollTop !== document.documentElement.offsetHeight || isFetching) return;
setIsFetching(true);
}
return [isFetching, setIsFetching];
};
export default useInfiniteScroll;
Using a custom React Hook has cut down the code in our List component by nearly half.
Our very skinny List component now looks like this:
import React, { useState } from 'react';
import useInfiniteScroll from "./useInfiniteScroll";
const List = () => {
const [listItems, setListItems] = useState(Array.from(Array(30).keys(), n => n + 1));
const [isFetching, setIsFetching] = useInfiniteScroll(fetchMoreListItems);
function fetchMoreListItems() {
setTimeout(() => {
setListItems(prevState => ([...prevState, ...Array.from(Array(20).keys(), n => n + prevState.length + 1)]));
setIsFetching(false);
}, 2000);
}
return (
<>
<ul className="list-group mb-2">
{listItems.map(listItem => <li className="list-group-item">List Item {listItem}</li>)}
</ul>
{isFetching && 'Fetching more list items...'}
</>
);
};
export default List;
Notice that we’re importing
Using our infinite scroll hook custom React Hook is one line. All we have to do is pass it the function that we want it to run. In our case, it’s the fetchMoreListItems function.
const [isFetching, setIsFetching] = useInfiniteScroll(fetchMoreListItems);
Wrapping Up
Well, there you have it. An infinite scroll list component built entirely using React Hooks.
We even got an added bonus of writing a custom React Hook to make our component nice and clean.
The entire codebase is over on our GitHub repository for this tutorial.
Comments
I’d avoid scroll events as much as possible, especially if they’re not debounced.
Use react-intersection-observer (https://www.npmjs.com/package/react-intersection-observer) which is a neat abstraction of the Intersection Observer API, and add the W3C’s polyfill so it falls back to scroll events *only* if necessary (https://github.com/w3c/IntersectionObserver/tree/master/polyfill).
Even better, use react-window instead of reinventing the wheel: https://github.com/bvaughn/react-window
And if you *MUST* listen to scroll events, please debounce them. https://css-tricks.com/the-difference-between-throttling-and-debouncing/
Hello James King, Thank you!!! But It’s not working on Safari. Please How to fix it 🙁
I like how simple this solution is. But like another commenter said, one should debounce the scroll event:
. . .
useEffect(() => {
window.addEventListener(‘scroll’, debounce(handleScroll, 500));
return () => window.removeEventListener(‘scroll’, debounce(handleScroll, 500));
}, []);
. . .
const debounce = (func, delay) => {
let inDebounce;
return function() {
clearTimeout(inDebounce);
inDebounce = setTimeout(() => {
func.apply(this, arguments);
}, delay);
}
}
Will definitely try this tonight, thank you James
If you replace `document.documentElement.scrollTop` with `Math.max(
window.pageYOffset,
document.documentElement.scrollTop,
document.body.scrollTop` in `handleScroll()` then this will work in Safari. Safari reports 0 for `document.documentElement.scrollTop` and that breaks this check.
This works for me with Intersection-Observer:
“`
function handleScroll() {
const observer = new IntersectionObserver(updateList, {
root: document.body,
rootMargin: “150px”,
threshold: 1,
});
const target = document.querySelector(“#newsList”);
observer.observe(target);
function updateList(entries, observer) {
setIsFetching(true);
}
}
“`
But what if i wanted to load more elements when i reach around 80% of the page. How can I calculate that ?
One way to achieve this is to calculate the offset pixels that are 80% of the total page pixels, and then make the load when the page reaches that threshold.
Hey yo! Great article! I made some modifications to work with React Native. Let me know what you think: https://gist.github.com/technoplato/e394369a6f202a58bf010635e6eb32c7
Thanks, Michael! The gist looks fantastic, great job. How are you liking Hooks in React Native?
Thanks for article. Very helpful.
thank you so mush for your great tutorials . after very search finaly find solution
How to have infinite scrolling with maximum value
How to have infinite scrolling with maximum value?