Custom React Hooks: useInView
Published: 2022-02-26 17:00:10

Last time, we’ve seen how to implement the [useAudio](https://dev.to/iamludal/custom-reack-hooks-useaudio-565j) Hook to simplify sounds management within our React apps. Today, let’s see how to create a Hook that tracks the visibility our components on the screen: `useInView`.

- [Motivation](#Motivation)
- [Implementation](#Implementation)
- [Usage](#Usage)
- [Wrapping Up](#Wrapping-Up)


## Motivation

First of all, let’s have a look at a concrete example to motivate our Hook implementation. The most obvious example is the one of infinite scrolls, like your Facebook or Instagram feed. You probably know that these applications don’t load entirely all of the content you see on your screen (otherwise, the DOM would contain thousands of elements, which would drastically impact the performance of the application). Instead, they load a certain amount of posts, typically a dozen, and wait for you to scroll in order to load a dozen more, until you scroll enough to load another dozen, and so on. This way, scrolling your feed feels like you are facing an infinite list.

To achieve this, they use something called [intersection observers](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) that wait for a given element to be above a given “threshold” (called the root margin), as shown in the figure below.

![Intersection Observers Diagram](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ith699172yjowutq5uda.png)

When more than the defined threshold (let’s say 25%) of the 4th element will be above the root margin, a callback function will be triggered in order to load more posts. This is what our custom Hook will be in charge of. 

If intersection observers are still confusing for you, don’t worry: I’ve set up a small demo project that you can play with to easily understand how they work. Just head over to this [CodeSandbox demo](https://codesandbox.io/s/intersection-observers-demo-16u0eg?file=/src/App.js) to try it out.

We can now get our hands dirty and jump into the Hook implementation. 👨🏻‍💻


## Implementation

Before implementing this new Hook, let’s discuss its signature: how do we want to use it? Typically, we want it to return a boolean value to know if the target element is visible or not.

```jsx
const isIntersecting = useInView()
```

But we have a first problem: as we’ve seen in the diagram of the previous section, the root margin is not directly the bottom of the screen. Instead, it will be an offset that we can manually set (like `100px`). We can also define the root element, from which the target element will be visible or not, as well as a visibility threshold for it (`0%`, `50%`, `100%`...). In our infinite scroll example, the root element corresponds to the browser viewport, the root margin could be set to `100px`, the threshold could be `0`, and the target element would be the footer of the page. This way, when the footer will reach `100px` from the bottom of the screen, the function for loading new posts will be triggered.

On the official documentation of intersection observers, we can see that an `options` object can be specified when creating a new instance. We will use the same object in order to keep the same logic instead of creating our own one, which could be confusing to our colleagues. Thus, our Hook signature can be changed to the following:

```jsx
const options = { root: someElement, rootMargin: '100px', threshold: 0.25 }
const isIntersecting = useInView(options)
```

> Each key of the `options` object is optional.

That’s way better, but we still have a last problem: where do we specify the target element to use? Actually, we don’t want to bother with the DOM native methods, such as `document.querySelector` or `document.findElementById`. Instead, we will use the React refs (references) to simplify this. The Hook will now have another parameter: the React reference of the target element.

```jsx
const target = useRef(null)
const options = { ... }
const isIntersecting = useInView(target, options)

...

<p ref={target}>Loading...</p>
```

> Note that the options parameter is optional, and could be omitted.

This looks awesome. We are now ready to dive in the actual implementation. Let’s first setup the Hook skeleton.

```jsx
const useInView = (target, options = {}) => {
  const [isIntersecting, setIsIntersecting] = useState(false);
  return isIntersecting;
}
```

Then, we are going to setup the intersection observer logic. We will create an instance of `IntersectionObserver` when the Hook is mounted. As we’ve seen previously, the constructor takes as a parameter a callback function, that will be executed when the target meets the threshold specified for the `IntersectionObserver`. This function receives as an argument the list of entries, each one being a threshold that was crossed by one of the observed elements (but in our case, we will only have one target element, so this list will only contain this single element). Hence our callback function is as simple as that:

```jsx
const callback = (entries) => {
  setIsIntersecting(entries[0].isIntersecting); 
}
```

Great. With this callback function and the options that we receive as arguments, we can now instantiate an `IntersectionObserver`.

```jsx
const useInView = (target, options = {}) => {
  const [isIntersecting, setIsIntersecting] = useState(false);
  const [observer, setObserver] = useState(null);

  useEffect(() => {
    const callback = (entries) => {
      setIsIntersecting(entries[0].isIntersecting);
    };

    const _observer = new IntersectionObserver(callback, options);
    setObserver(_observer);
  }, []);

  return isIntersecting;
}
```

So far, nothing happens. This is because we need to actually observe the given target after creating the instance, so that the callback gets called accordingly.

```jsx
useEffect(() => {
  const callback = (entries) => {
    setIsIntersecting(entries[0].isIntersecting);
  };

  const _observer = new IntersectionObserver(callback, options);
  _observer.observe(target);
  setObserver(_observer);
}, []);
```

Awesome. However, we need to be careful: when our Hook is unmounted, we have to stop watching the target element visibility changes so that the callback is not accidentally called. To do so, we just have to return a cleanup function inside the `useEffect` Hook that will take care of disconnecting from the observer when the Hook is unmounted.

```jsx
useEffect(() => {
  const callback = (entries) => {
    setIsIntersecting(entries[0].isIntersecting);
  };

  const _observer = new IntersectionObserver(callback, options);
  _observer.observe(target);
  setObserver(_observer);

  return () => {
    observer?.disconnect();
  };
}, []);
```

> ℹ️ We are using the optional chaining operator (`?.`) to prevent from errors if for some reasons the observer is still `null`.
> 

That’s way better! We are getting close to our goal. We just have one last thing to deal with: we have to listen for value changes of the observer arguments. For example, if the target element or the threshold are changed, we have to update our observer accordingly. To do so, we just have to add them to the dependency array of the `useEffect` Hook, where we will disconnect from the previous observer (if it exists), and recreate a new instance with updated values.

```jsx
useEffect(() => {
  const callback = (entries) => {
    setIsIntersecting(entries[0].isIntersecting);
  };

  observer?.disconnect(); // Disconnect from the previous observer

  // target.current can be null, in which case we do nothing
  if (target.current) {
    const _observer = new IntersectionObserver(callback, options);
    _observer.observe(target);
    setObserver(_observer);
  }

  return () => {
    observer?.disconnect();
  };
}, [target.current, options.root, options.rootMargin, options.threshold]);
```

And that's it, we are now done with our Hook. Here is the final implementation.

```jsx
const useInView = (target, options = {}) => {
  const [isIntersecting, setIsIntersecting] = useState(false);
  const [observer, setObserver] = useState(null);

  useEffect(() => {
    const callback = (entries) => {
      setIsIntersecting(entries[0].isIntersecting);
    };

    observer?.disconnect();

    if (target.current) {
      const _observer = new IntersectionObserver(callback, options);
      _observer.observe(target);
      setObserver(_observer);
    }

    return () => {
      observer?.disconnect();
    };
  }, [target.current, options.root, options.rootMargin, options.threshold]);

  return isIntersecting;
}
```


## Usage

In the first part of this article, we’ve talked about Facebook to introduce intersection observers. We are going to simulate a competitor of it, *Fuzebook*. In the following snippet, we load the user’s feed. The target element for our intersection observer will be a paragraph at the bottom of the page, containing “Loading more posts...”. We could also have used another element as the target, such as a spinner or even the page footer. Either way, the target shouldn’t be visible since we’ve defined a root margin of 150px. In fact, it could be visible if the `fetch` call (`getPosts`) takes a few seconds.

In our case, the scroll is infinite: we are never going to reach the bottom of the page since our Facebook feed typically contains hundreds (if not thousands) of posts. If your scrolling section contains fewer elements (let’s say a hundred), keep in mind that you can handle the case where you have no more elements to load (in which case you would just hide the `target` element).

Below is the code of the main component of our *Fuzebook* application.

```jsx
const App = () => {
  const posts = useArray([]);
  const page = useCounter(1);
  const loadingElement = useRef(null);
  const isIntersecting = useInView(loadingElement, {
    threshold: 1,
    rootMargin: "150px"
  });

  // Load next page posts
  useEffect(() => {
    getPosts(page.value).then((newPosts) => {
      posts.concat(newPosts);
    });
  }, [page.value]);

  useEffect(() => {
    if (isIntersecting) {
      page.increment();
    }
  }, [isIntersecting]);

  return (
    <div className="App">
      <h1>Fuzebook</h1>
      <ul>
        {posts.value.map((post, i) => (
          <Card key={i} post={post} />
        ))}
      </ul>
      <p ref={loadingElement}>Loading more posts...</p>
    </div>
  );
}
```

As you can see, we’ve reused two Hooks that we’ve implemented in previous articles: [useArray](https://dev.to/iamludal/custom-react-hooks-usearray-101g) and [useCounter](https://dev.to/iamludal/custom-react-hooks-usecounter-5a73). This simplifies our `App` component, resulting in a very clean and readable code. The `Card` component renders a basic post card, and the `getPosts` function (that returns a promise) just fetches any rest API to get some posts.


## Wrapping Up

This article closes the [Custom React Hooks](https://dev.to/iamludal/series/14964) series, during which we’ve discovered how to extract some logic into reusable functions in order to simplify our code. This also allowed us to reuse common logic through our application, avoiding code duplication. By doing so, we were following the Single Responsibility Principle for both our components (that now only focus on their responsibility) and our Hooks. I hope you enjoyed following this series as much as I enjoyed writing it, and I’ll see you in upcoming articles. 👋

---

Source code available on [CodeSandbox](https://codesandbox.io/s/custom-react-hooks-useinview-6cev5y?file=/src/App.tsx).


{% user iamludal %}