Creating an Infinite Scroll Hook

Henrique Ramos - Oct 2 '20 - - Dev Community

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.

. . . . . . . . . . . . . . . . . . .
Terabox Video Player