DEV Community 👩‍💻👨‍💻

Cover image for Creating an Infinite Scroll Hook
Henrique Ramos
Henrique Ramos

Posted on • Updated on

Creating an Infinite Scroll Hook

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);
  });
}
Enter fullscreen mode Exit fullscreen mode

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]);
}
Enter fullscreen mode Exit fullscreen mode

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];
}
Enter fullscreen mode Exit fullscreen mode

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];
}
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
thomasledoux1 profile image
Thomas Ledoux

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...

Collapse
 
hnrq profile image
Henrique Ramos Author • Edited on

Nice! Didn't know about that, will definitely take a look! Thanks for the suggestion

Collapse
 
theonlybeardedbeast profile image
TheOnlyBeardedBeast • Edited on

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?

Collapse
 
hnrq profile image
Henrique Ramos Author • Edited on

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

Collapse
 
theonlybeardedbeast profile image
TheOnlyBeardedBeast

Yeah it was strange, now it looks good 🙂 nice job

Thread Thread
 
hnrq profile image
Henrique Ramos Author

Thanks a lot

🌚 Life is too short to browse without dark mode