DEV Community

loading...
Cover image for Creating an Infinite Scroll Hook

Creating an Infinite Scroll Hook

hnrq profile image Henrique Ramos Updated on ・3 min read

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.

Discussion (6)

pic
Editor guide
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

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

Collapse
theonlybeardedbeast profile image
TheOnlyBeardedBeast • Edited

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

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