Creating an Infinite Scroll Hook
Tags: react, javascript
Published: 2020-10-02 06:51:03

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:
```jsx
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:
```jsx
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`:
```jsx
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:
```jsx
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](https://vimeo.com/258960103)", by Alon Sivan.**