In React apps, sometimes it is important to know when your user has scrolled to the bottom of a page.
In apps where you have an infinite scroll, such as Instagram for example, once the user hits the bottom of the page, you need to fetch more posts.
Let’s take a look at how to create a usePageBottom hook ourselves for similar use cases like creating an infinite scroll.
We’ll begin by making a separate file, usePageBottom.js, in our utils folder and add a function (hook) with the same name:
// utils/usePageBottom.js
import React from "react";
export default function usePageBottom() {}
Next, we’ll need to calculate when our user hits the bottom of the page. We can determine this with information from the window
. In order to access this, we’re going to need to make sure our component that the hook is called within is mounted, so we’ll use the useEffect
hook with an empty dependencies array.
// utils/usePageBottom.js
import React from "react";
export default function usePageBottom() {
React.useEffect(() => {}, []);
}
The user will have scrolled to the bottom of the page when the window’s innerHeight
value plus the document’s scrollTop
value is equal to the offsetHeight
. If those two values are equal, the result will be true, and the user has scrolled to the bottom of the page:
// utils/usePageBottom.js
import React from "react";
export default function usePageBottom() {
React.useEffect(() => {
window.innerHeight + document.documentElement.scrollTop ===
document.documentElement.offsetHeight;
}, []);
}
We’ll store the result of this expression in a variable, isBottom
and we’ll update a state variable called bottom
, which we’ll ultimately return from our hook.
// utils/usePageBottom.js
import React from "react";
export default function usePageBottom() {
const [bottom, setBottom] = React.useState(false);
React.useEffect(() => {
const isBottom =
window.innerHeight + document.documentElement.scrollTop ===
document.documentElement.offsetHeight;
setBottom(isButton);
}, []);
return bottom;
}
Our code as is, however, won’t work. Why not?
The issue lies in the fact that we need to calculate isBottom
whenever the user is scrolling. As a result, we need to listen for a scroll event with window.addEventListener
. We can reevaluate this expression by creating a local function to be called whenever the user scroll, called handleScroll
.
// utils/usePageBottom.js
import React from "react";
export default function usePageBottom() {
const [bottom, setBottom] = React.useState(false);
React.useEffect(() => {
function handleScroll() {
const isBottom =
window.innerHeight + document.documentElement.scrollTop
=== document.documentElement.offsetHeight;
setBottom(isButton);
}
window.addEventListener("scroll", handleScroll);
}, []);
return bottom;
}
Finally, since we have an event listener that is updating state, we need to handle the event that our user navigates away from the page and our component is removed. We need to remove the scroll event listener that we added, so we don’t attempt to update a state variable that no longer exists.
We can do this by returning a function from useEffect
along with window.removeEventListener
, where we pass a reference to the same handleScroll
function. And we’re done.
// utils/usePageBottom.js
import React from "react";
export default function usePageBottom() {
const [bottom, setBottom] = React.useState(false);
React.useEffect(() => {
function handleScroll() {
const isBottom =
window.innerHeight + document.documentElement.scrollTop
=== document.documentElement.offsetHeight;
setBottom(isButton);
}
window.addEventListener("scroll", handleScroll);
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, []);
return bottom;
}
Now we can simply call this code in any function where we want to know whether we’ve hit the bottom of the page or not. Here is one example of how I’ve used it in a Feed component that uses an infinite scroll:
// components/Feed.js
import React from "react";
import usePageBottom from "../utils/usePageBottom";
function Feed() {
const isPageBottom = usePageBottom();
React.useEffect(() => {
// if no data or user hasn't scroll to the bottom, don't get more data
if (!isPageBottom || !data) return;
// otherwise, get more posts
const lastTimestamp = data.posts[data.posts.length - 1].created_at;
const variables = { limit: 20, feedIds, lastTimestamp };
fetchMore({
variables,
updateQuery: handleUpdateQuery,
});
}, [isPageBottom, data, fetchMore, handleUpdateQuery, feedIds]);
}
Feel free to use this hook yourself in your own client-rendered React apps!
Become a React Developer in 5 Weeks
React is hard. You shouldn't have to figure it out yourself.
I've put everything I know about React into a single course, to help you reach your goals in record time:
Introducing: The React Bootcamp
It’s the one course I wish I had when I started learning React.
Click below to try the React Bootcamp for yourself:
Top comments (3)
Hey Reed. This is bloody brilliant! In the first few lines of this, I already found myself looking up scrollTop in the MDN. Really intriguing.
(there is a useState typograpical error, where you say setIsBottom(isButton), and I suspect you intended to write setIsBottom(isBottom)).
Excellent stuff!
Clear and concise. Love the simplicity
Beautiful, thnks for sharing!