useOnScreen() hook in React

Tracking the component’s visibility can be really handy in multiple cases, especially for performance, when you want to load to the media like image, video, audio, etc. only when the component is in the viewport or is visible.

Another case is when you want to track the user activity like when the user is a starring a product (product is in the viewport) so that you can use this data for recommendations.

For this, we can create a useOnScreen() hook that will return a boolean value if the component or HTML element is the viewport or not in Reactjs.

HTML element in the viewport in React - useOnscreen() hook

There are two ways to implement it.
1. Using Intersection Observer.
2. Using getBoundingClientRect().

Using Intersection Observer

With useRef() we will create a reference to the DOM element which we want to track, and then pass this to the useOnScreen() hook.

useOnScreen() hook will set up the observation for the ref when the component will be mounted. This is will be done in the useEffect() and then create an instance of IntersectionObserver and if the entry is interacting, update the state to true which means it visible, otherwise false.

Disconnect the observation when the component is about to unmount inside the useEffect().

function useOnScreen(ref) {
  const [isIntersecting, setIntersecting] = useState(false);

  // monitor the interaction
  const observer = new IntersectionObserver(
    ([entry]) => {
      // update the state on interaction change
      setIntersecting(entry.isIntersecting);
    },{
      threshold: 1.0
    }
  );

  useEffect(() => {
    // assign the observer
    observer.observe(ref.current);

    // remove the observer as soon as the component is unmounted
    return () => {
      observer.disconnect();
    };
  }, []);

  return isIntersecting;
}

Test Case

const Element = ({ number }) => {
  const ref = useRef();
  const isVisible = useOnScreen(ref);

  return (
    <div ref={ref} className="box">
      {number}
      {isVisible ? `I am on screen` : `I am invisible`}
    </div>
  );
};

const DummyComponent = () => {
  const arr = [];
  for (let i = 0; i < 20; i++) {
    arr.push(<Element key={i} number={i} />);
  }

  return arr;
};

export default DummyComponent;

Using getBoundingClientRect()

Unlike Intersection Observer, here we will have to perform simple calculations to determine if the element is in the viewport or not.

If the top of the element is greater than zero but less than the window.innerHeight then it is in the viewport. We can also add some offset in case we want a buffer.

Assign a scroll event on the window and inside the listener get the getBoundingClientRect() of the element. Perform the calculation and update the state accordingly.

function useOnScreen2(ref) {
  const [isIntersecting, setIntersecting] = useState(false);
  
  // determine if the element is visible
  const observer = function () {
    const offset = 50;
    const top = ref.current.getBoundingClientRect().top;
    setIntersecting(top + offset >= 0 && top - offset <= window.innerHeight);
  };

  useEffect(() => {
    // first check
    observer();

    // assign the listener
    window.addEventListener("scroll", observer);

    // remove the listener
    return () => {
      window.removeEventListener("scroll", observer);
    };
  }, []);

  return isIntersecting;
}

Test Case

const Element = ({ number }) => {
  const ref = useRef();
  const isVisible = useOnScreen2(ref);

  return (
    <div ref={ref} className="box">
      {number}
      {isVisible ? `I am on screen` : `I am invisible`}
    </div>
  );
};

const DummyComponent = () => {
  const arr = [];
  for (let i = 0; i < 20; i++) {
    arr.push(<Element key={i} number={i} />);
  }

  return arr;
};

export default DummyComponent;