If you've ever used a mobile app, chances are high that you ran across an Infinite Scroll. Basically, you scroll and, at a given DOM height, something happens. Twitter, for instance, will fetch new posts when you reach the bottom.
Hooks were a game-changer for React: now Functional Components can have state and lifecycle methods. A custom hook can also be reused to add a behavior to an Element, which is finally a good alternative for HOC and its "Wrapper Hell". So, today I'm going to teach you how to create a React Hook to implement this feature.
Let's Go!
We are going to start by defining what this hook should do. So the first thing to do is to add an event listener to window
, since we're going to spy its scrollHeight
, so:
import { useEffect, useState } from 'react';
const useInfiniteScroll = (callback: Function) => {
useEffect(() => {
window.addEventListener('scroll', callback);
return () => window.removeEventListener('scroll', callback);
});
}
The Threshold
Now, the callback
function will be called everytime the page is scrolled which isn't the desired behavior. So we need to add a threshold for it to be triggered after crossing it. This will be provided through a parameter, which its value should be between 0 and 1:
import { useEffect, useState } from 'react';
const useInfiniteScroll = (callback: Function, threshold: number = 1) => {
useEffect(() => {
const handleScroll = () => {
if (window.innerHeight + document.documentElement.scrollTop
>= document.documentElement.offsetHeight * threshold)
callback();
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, [callback]);
}
A strange bug
The core is basically done. However, if you keep scrolling after crossing the "trigger point", you'll notice that the callback
is being called multiple times. It happens because we should assure that it'll be called after this scroll height, as well as it's going to happen once. To do so, we can add isFetching
:
import { useEffect, useState } from 'react';
const useInfiniteScroll = (callback: Function, threshold: number = 1) => {
const [isFetching, setIsFetching] = useState<Boolean>(false);
useEffect(() => {
const handleScroll = () => {
if (window.innerHeight + document.documentElement.scrollTop
>= document.documentElement.offsetHeight * threshold
&& !isFetching) {
setIsFetching(true);
callback();
}
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, [isFetching, callback]);
return [setIsFetching];
}
We are going to return setIsFetching
so that we can control whether or not the callback finished fetching.
Last, but not least
Most of the time, an infinite scroll isn't actually infinite. So, when there's no more data to be fetched, the event listener isn't needed anymore, so it's nice to remove it:
import { useEffect, useState } from 'react';
const useInfiniteScroll = (callback: Function, threshold: number = 1) => {
const [isFetching, setIsFetching] = useState<Boolean>(false);
const [isExhausted, setIsExhausted] = useState<Boolean>(false);
useEffect(() => {
const handleScroll = () => {
if (window.innerHeight + document.documentElement.scrollTop
>= document.documentElement.offsetHeight * threshold
&& !isFetching) {
setIsFetching(true);
callback();
}
};
if (isExhausted) window.removeEventListener('scroll', handleScroll);
else window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, [isFetching, isExhausted, callback]);
return [setIsFetching, isExhausted, setIsExhausted];
}
Now, we are also returning isExhausted
and setIsExhausted
. The first one could be used for rendering a message and the second to tell the hook that the there's no more data to be fetched.
That's it
And that's it, guys. Hopefully I could enlighten your path on implementing this feature. This approach has worked as a charm for me, even though it may not be the fanciest.
PS: The cover was taken from "How To Love - Three easy steps", by Alon Sivan.
Top comments (6)
Nice implementation!
I think you might be able to simplify this by using the Intersection API (developer.mozilla.org/en-US/docs/W...). This has the threshold built in, you can pass a rootElement, rootMargin... This article explains how you could implement this: dev.to/somtougeh/building-infinite...
Nice! Didn't know about that, will definitely take a look! Thanks for the suggestion
Does it really work to use a useState inside a useEffect? Also how can you return those values outside the useeffect if they are initialized inside the useEffect?
Oh!! What a shame! No it doesn't work, I just edited the text to fix it. Maybe I got lost while editing the steps :P
Yeah it was strange, now it looks good 🙂 nice job
Thanks a lot