---
title: "How to detect changes in component visibility when scrolling?"
description:
  "Learn how to detect an element being scrolled into and scrolled out of the
  parent element's visible area in React using IntersectionObserver."
canonical_url: "https://www.bigbinary.com/blog/how-to-detect-changes-in-component-visibility-when-scrolling"
markdown_url: "https://www.bigbinary.com/blog/how-to-detect-changes-in-component-visibility-when-scrolling.md"
---

# How to detect changes in component visibility when scrolling?

Learn how to detect an element being scrolled into and scrolled out of the
parent element's visible area in React using IntersectionObserver.

- Author: Amaljith K
- Published: September 27, 2022
- Categories: JavaScript

When there is a need to display a large set of data, most of the web
applications split the whole set into several smaller chunks and then serve them
on demand. This technique is called pagination.

Earlier, pagination looked like this:

![image](https://user-images.githubusercontent.com/85148587/191435753-53aa1d13-3d55-42f2-b95d-922b6e0e3b7f.png)

Here, loading the next set of data required the user to click on the next page
button.

These days, we use the infinite scroll technique, which automatically loads the
next set of data when the user scrolls to the bottom of the list. This is more
user-friendly:

![image](https://user-images.githubusercontent.com/85148587/191432751-89eef3dc-5c5e-4939-8150-38dc98cee262.gif)

Several JS libraries are available to facilitate infinite scroll. But to quench
our curiosity about how things work under the hood, it is best to try to
implement it from scratch.

To implement infinite scroll, we need to know when the user has scrolled to the
bottom of the list to load the next page's data. To know if the user has reached
the bottom, we can watch the last element of the list. That is, when the list is
scrolled and the last element becomes visible, we know that we are at the
bottom.

Detecting the visibility of elements during scroll was a hard problem until
recently. We had to hook onto `onscroll` events of the element and check the
boundaries of the elements using
[getBoundingClientRect](https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect)
function.

Since `onscroll` event gets fired around 40-50 times per second, performing the
operations inside it will become expensive. Moreover, the `onscroll` function
gets executed from the main UI thread. All these together make our web
application sluggish.

But now, we have a much more performant alternative for this problem.
[All popular web browsers](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API#browser_compatibility)
support a new API named
[IntersectionObserver](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API)
from 2019 onwards.

The advantages of `IntersectionObserver` API are:

- It doesn't grab the resources from the UI thread. It accepts a callback
  function that will be fired **asynchronously**.
- The supplied callback is triggered only when a change in visibility is
  detected. We can save 40-50 repetitions per second during the scroll.
- We don't need to worry about maintaining boilerplate code for detecting the
  boundaries & calculating the visibility. We get all useful data as a parameter
  to the callback function.

The introduction of `IntersectionObserver` simplified a whole set of
requirements like:

- Infinite loading.
- Improving page load time by not fetching resources (like images) that aren't
  visible until the user scrolls to it. This is called lazy loading.
- Track whether the user has scrolled to and viewed an ad posted on the web
  page.
- UX improvements like dimming animation for the components that aren't fully
  visible.

In this blog, we are going to discuss how we can use `IntersectionObserver` in a
React application as hooks.

## Creating a hook for detecting visibility changes

We will create a custom hook that will update whenever the specified component
scrolls into view and scrolls out of view. Let us name the hook
`useIsElementVisible`. Obviously, it will accept a reference to the component of
which visibility need to be monitored as its argument.

It will have a state to store the visibility status of the specified element. It
will have a useEffect hook from which we will bind the `IntersectionObserver` to
the specified component.

Here is the basic implementation:

```js
import { useEffect, useState } from "react";

const useIsElementVisible = target => {
  const [isVisible, setIsVisible] = useState(false); // store visibility status

  useEffect(() => {
    // bind IntersectionObserver to the target element
    const observer = new IntersectionObserver(onVisibilityChange);
    observer.observe(target);
  }, [target]);

  // handle visibility changes
  const onVisibilityChange = entries => setIsVisible(entries[0].isIntersecting);

  return isVisible;
};

export default useIsElementVisible;
```

We can use `useIsElementVisible` like this:

```jsx
const ListItem = () => {
  const elementRef = useRef(null); // to hold a reference to the component we need to track

  const isElementVisible = useIsElementVisible(elementRef.current);

  return (
    <div ref={elementRef} id="list-item">
      {/* your component jsx */}
    </div>
  );
};
```

The component `ListItem` will get updated whenever the user scrolls to see the
div `"list-item"`. We can use the value of `isElementVisible` to load the
contents of the next page from a `useEffect` hook:

```js
useEffect(() => {
  if (isElementVisible && nextPageNotLoadedYet()) {
    loadNextPage();
  }
}, [isElementVisible]);
```

**This works in theory.** But if you try it, you will notice that this doesn't
work as expected. We missed an edge case.

## The real-life edge case

We use a `useRef` hook for referencing the `div`. During the initial render,
`elementRef` was just initialized with `null` as its value. So,
`elementRef.current` will be null and as a result, the call
`useIsElementVisible(elementRef.current)` won't attach our observer to the
element for the first time.

Unfortunately, useRef hook won't re-render when a value is set to it after DOM
is prepared. Also, there are no state updates or anything that requests a
re-render inside our example component. In short, our component will render only
once.

With these in place, `useIsElementVisible` will never get a reference to the
`"list-item"` div in our previous example.

But there is a workaround for our problem. We can force render the component
twice during the first mount.

To make it possible, we will add a dummy state. When our hook is called for the
first time (when `ListItem` mounts), we will update our state once, thereby
requesting React to repeat the component render steps again. During the second
render, we will already have our DOM ready and we will have the target element
attached to `elementRef`.

## Force re-rendering the component

To keep our code clean and modular, let us create a dedicated custom hook for
managing force re-renders:

```js
import { useState } from "react";

const useForceRerender = () => {
  const [, setValue] = useState(0); // we don't need the value of this state.
  return () => setValue(value => value + 1);
};

export default useForceRerender;
```

Now, we can use it in our `useIsElementVisible` hook this way:

```js {4-8}
const useIsElementVisible = target => {
  const [isVisible, setIsVisible] = useState(false);

  const forceRerender = useForceRerender();

  useEffect(() => {
    forceRerender();
  }, []);

  // previous code to register observer

  return isIntersecting;
};
```

With this change, our hook is now self-sufficient and fully functional. In our
`ListItem` component, `isElementVisible` will update to `false` and trigger
component re-render whenever our `"list-item"` div goes outside visible zone
during scroll. It will also update to `true` when it is scrolled into visibility
again.

## Possible improvements on useIsElementVisible hook

The `useIsElementVisible` hook shown in the previous sections serves only the
basic use case. It is not optimal to use in a production world.

These are the scopes for improvement for our hook:

- We can let in
  [configurations for IntersectionObserver](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API#intersection_observer_options)
  to customize its behavior.
- We can prevent initializing observer when the `target` is not ready yet (when
  it is `null`).
- We can add a cleanup function to stop observing the element when our component
  gets unmounted.

Here is what the optimum code for the hook should look like:

```js
import { useEffect, useState } from "react";

export const useForceRerender = () => {
  const [, setValue] = useState(0); // we don't need the value of this state.
  return () => setValue(value => value + 1);
};

export const useIsElementVisible = (target, options = undefined) => {
  const [isVisible, setIsVisible] = useState(false);
  const forceUpdate = useForceRerender();

  useEffect(() => {
    forceUpdate(); // to ensure that ref.current is attached to the DOM element
  }, []);

  useEffect(() => {
    if (!target) return;

    const observer = new IntersectionObserver(handleVisibilityChange, options);
    observer.observe(target);

    return () => observer.unobserve(target);
  }, [target, options]);

  const handleVisibilityChange = ([entry]) =>
    setIsVisible(entry.isIntersecting);

  return isVisible;
};
```

## Links

- [Human page](https://www.bigbinary.com/blog/how-to-detect-changes-in-component-visibility-when-scrolling)
