Detect if a React Component is Out of Viewport Bounds
Published: 2021-01-26 18:11:16

Photo by [Erol Ahmed](<https://unsplash.com/@erol?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText>) on [Unsplash](<https://unsplash.com/s/photos/hidden?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText>)

So I recently found myself asking, "How do I detect if a component is out of viewport bounds?", the reason I was asking myself this question was because I wanted to know how to detect and move components if they were not in view, such as tooltips, dropdowns, menu items etc...

At first I thought the `useLayoutEffect` hook might have been a good option however it would not work for a component that was, for example, draggabble unless you were able to hook (😃) into the dragging state and add that as a dependency of the hook. It also wouldn't really work for anything that was controlled by CSS such as animations or hover effects either.

So I kept researching, googling, and testing things out until I came across the JavaScript `MutationObserver`.

> A built-in object that observes a [DOM](<https://www.w3docs.com/learn-javascript/javascript-with-dom.html>) element, firing a [callback](<https://www.w3docs.com/learn-javascript/introduction-callbacks.html>) in case of modifications - W3 Schools

I'm not going to pretend to be a `MutationObserver` expert but I'll link to [this article](<https://dev.opera.com/articles/mutation-observers-tutorial/>) in case you want to learn more about it.

Basically we're going to utilize the `MutationObserver` to watch for changes to a component and then we're going to check the components position.



## On wards to the code! ⚔️



### Basic Hook Setup 🏗️

If you haven't written a hook before you may be a little intimidated but they are actually pretty simple. A custom hook is just a function that can utilize/call other React Hooks.

```JavaScript
export const useOutOfBounds = () => {
	// Cool code goes here 😃
};
```

Since our hook isn't going to accept an parameters we're not going to include them in the function definition. One thing to note here is the function name, `useOutOfBounds` , React requires that custom hook names start with `use` otherwise it will throw obscure errors.

### Effect, State, Observer 🕵🏽‍♂️, and Hook Return

Since this is a custom hook we can call other hooks, such as `useState`, `useRef`, and `useEffect` which is precisely what we will be doing.

```JavaScript
export const useOutOfBounds = () => {
	const componentRef = React.useRef();
 	const [isOutOfBounds, setIsOutOfBounds] = React.useState({
		top: 0,
		bottom: 0,
		left: 0,
		right: 0
 	});
 	const observer = new MutationObserver(mutationObserverCallback); //We'll define this callback in a second
    
    React.useEffect(() => {
		if (componentRef.current) {
			observer.observe(componentRef.current, { attributes: true, childList: true, subtree: true });
		}

		return () => observer.disconnect();
	}, [componentRef, observer]);

	return [componentRef, isOutOfBounds];

};
```

- `useRef` is used (😂) to create a ref that the user of the hook will pass to the component we will be observing.
- `useState` will store the information about the out of bounds positioning of the component
- `useEffect` is used to kick off the observation and cleanup when the component is mounted and unmounted

<!-- -->

We are also creating a `MutationObserver` and passing it a callback, don't worry we'll setup the callback in just a moment . Within the `useEffect` we are invoking the `observe` method of the `MutationObserver` we created when the component ref has been updated, this is what tells the observer to start observing/watching the dom element/component for changes. Lastly, within the `useEffect` we simply invoke the `disconnect` method of the `MutationObserver` to stop watching the component on cleanup (we don't want stray observers running).

The last thing we did was define the return for the hook, in typical hook fashion we are returning an array, where the first item is the component ref that the user will pass to their component and the second is the object detailing how far out of bounds the component is.

### Observer callback setup

Now lets create the callback that will be passed to the `MutationObserver`, this is going to do all of the work. I'm not going to repeat the rest of the hook code from above but this code should be within the scope of the hook function.

```JavaScript
const mutationObserverCallback = (mutationRecord, observer) => {
    if (componentRef.current) {
      const rect = componentRef.current.getBoundingClientRect();
      const windowWidth = Math.min(document.documentElement.clientWidth, window.innerWidth);
      const windowHeight = Math.min(document.documentElement.clientHeight, window.innerHeight);
      let directions = {
        top: 0,
        bottom: 0,
        left: 0,
        right: 0
      };

      if (rect.top < 0) {
        directions.top = Math.abs(0 - rect.top);
      } 

      if (rect.bottom > windowHeight) {
        directions.bottom = Math.abs(windowHeight - rect.bottom);
      }

      if (rect.left < 0) {
        directions.left = Math.abs(0 - rect.left);
      }

      if (rect.right > windowWidth) {
        directions.right = Math.abs(windowWidth - rect.right);
      }

      if (isOutOfBounds.top !== directions.top || isOutOfBounds.bottom !== directions.bottom || isOutOfBounds.left !== directions.left || isOutOfBounds.right !== directions.right) {
        setIsOutOfBounds(directions);
      }
    }
  }
```

Phew 💨 that's a lot of code, let me explain!

- `rect` basically is the position of the component we are watching. where top, right, left, and bottom are the distance of the component from those various sides of the viewport. ([Mozilla Docs](<https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect>))
- `windowWidth` and `windowHeight` are pretty self explanatory

<!-- -->

After we define those variables we just go through some simple logic in the if statements and get the amount of **pixels** the component is out of viewport on the various sides.

Lastly we do a comparison of the properties of the current out of bounds and the new one before setting it.

## Time to Use the Hook 🥳

Its actually quite simple, all you need to do is import the hook like so:

```JavaScript
import { useOutOfBounds } from './path-to-hook'
```

Then call the hook like this (Remember the returned array from the hook, well we're destructuring it here):

```JavaScript
const [componentRef, outOfBounds] = useOutOfBounds();
```

## Demo

This is a gif of a quick demo that I made in a CRA app using this hook. You'll see the out of bounds object logged to the console as the text is moved slightly out of the viewport.

<figure class="kg-card kg-image-card"><img src="https://ghost.ameersami.com/content/images/2021/01/Screen-Recording-2021-01-26-at-1.gif" class="kg-image"></figure>

## We're Done! 🎉

We made a custom hook! If it was your first time doing so, congrats! If not hopefully you are able to use this hook in some future projects!