How to use Resize Observer with React

murashow

Makar Murashov

Posted on July 9, 2022

How to use Resize Observer with React

In the first part of the Web APIs series Quick guide to Resize Observer we've learnt what the Resize Observer API is and how to use it with vanilla JavaScript. But what to do when it comes to using with React?
Today we are going to see how to do it quick & easy and will create a custom hook, which you can use in your projects.

The API

Let's repeat what we know already:

  • ResizeObserver is used to observe changes to Element's size,
  • to create our own observer instance we call the ResizeObserver constructor passing the callback function that will be fired every time, when the size changes:
const myObserver = new ResizeObserver(
  (entries: ResizeObserverEntry[], observer: ResizeObserver) => {
    for (let entry of entries) {
      // Do something with an entry (see in next section)
    }
});
Enter fullscreen mode Exit fullscreen mode
  • to start / stop watching the Element's size we shall invoke observe / unobserve instance's methods:
const myElement = document.getElementById('my-element');
myObserver.observe(myElement); // Start watching
myObserver.unobserve(myElement); // Stop watching
Enter fullscreen mode Exit fullscreen mode
  • each observed entry contains entry's element sizes info:
interface ResizeObserverEntry {
  readonly target: Element;
  readonly borderBoxSize: ReadonlyArray<ResizeObserverSize>;
  readonly contentBoxSize: ReadonlyArray<ResizeObserverSize>;
  readonly devicePixelContentBoxSize: ReadonlyArray<ResizeObserverSize>;
  readonly contentRect: DOMRectReadOnly; // May be deprecated, don't use it!
}

interface ResizeObserverSize {
    readonly blockSize: number;
    readonly inlineSize: number;
}
Enter fullscreen mode Exit fullscreen mode

The Task

Next we want to use our knowledge to get the sizes in any React app. There is no better solution than creating a React Hook, which could be used across all project in any component.
So let's try to define what exactly we want from the hook:

  1. It should be universal and its usage must be as simple as const size = giveMeMySize();
  2. As you've probably seen (I hope 😄) in the previous section, one ResizeObserver instance is able to handle any amount of elements. If we want to keep our app performant the hook should use only single observer instance inside;
  3. In order make the hook handy to use it should deal with mutations itself and automatically stop observing on unmount;
  4. Keep in mind that although the ResizeObserver API already has broad support, it is still in Editor’s Draft and isn't supported by all browsers. It's better to provide a fallback for it.

The Solution

Our requirements look good and pretty strict, uh? But don't worry, we can deal with all of them using the beautiful and very easy-to-use useResizeObserver hook from the beautiful react-hook library by Jared Lunde. According to it's documentation and my tests and usage as well it meets all our requirements:

  • Uses a single ResizeObserver for tracking all elements used by the hooks. This approach is astoundingly more performant than using a ResizeObserver per element which most hook implementations do,
  • Uses @juggle/resize-observer as a ponyfill when ResizeObserver isn't supported by the current browser,
  • automatically unobserves the target element when the hook unmounts,
  • you don't have to wrap your callback in useCallback() because any mutations are handled by the hook.

Feels promising, why don't we use it right now?

The Hook

We are ready to create our hook finally. First of all, install the useResizeObserver hook:

npm install @react-hook/resize-observer
// Or
yarn install @react-hook/resize-observer
Enter fullscreen mode Exit fullscreen mode

Then let's define how our hook will look like:

// useElementSize.ts
import { MutableRefObject, useLayoutEffect, useRef, useState } from 'react';
import useResizeObserver from '@react-hook/resize-observer';

interface Size {
  width: number;
  height: number;
}

export default function useElementSize<T extends HTMLElement = HTMLDivElement>(): [MutableRefObject<T | null>, Size] {
  const target = useRef<T | null>(null);
  const [size, setSize] = useState<Size>({
    width: 0,
    height: 0,
  });

  return [target, size]
}
Enter fullscreen mode Exit fullscreen mode

As you see, we've created the useElementSize function, which we can provide a generic type of our Element. It creates the target reference to connect to the React element inside a component and the size for the current Element's size state, which implements the Size interface.

Keep in mind that while Element is resizing, its dimensions can be (and usually are) decimal numbers. We can round them of course:

const setRoundedSize = ({ width, height }: Size) => {
  setSize({ width: Math.round(width), height: Math.round(height) });
};
Enter fullscreen mode Exit fullscreen mode

Next, we need to set the initial size of the Element. This is where the React useLayoutEffect hook fits perfectly. It fires before the browser paint, allowing us to get the Element's dimensions using its getBoundingClientRect method:

useLayoutEffect(() => {
    target.current && setRoundedSize(target.current.getBoundingClientRect())
}, [target]);
Enter fullscreen mode Exit fullscreen mode

And last but not least, let's put some magic (not) there with the help of the useResizeObserver hook, that will trigger the size update each time the target's size changes:

useResizeObserver(target, entry => {
  const { inlineSize: width, blockSize: height } = entry.contentBoxSize[0];
  setRoundedSize({ width, height });
});
Enter fullscreen mode Exit fullscreen mode

And the usage 🤠

Let's try to repeat the exercise from the first part Quick guide to Resize Observer of the series, let me remind you the task:

Say we have a box of strawberries and getting them bigger makes us really happy (and vice versa).

I won't go into detailed explanations, because as you remember, our goal was to create a hook that is very easy to use. You can check the code and how it works all together below.
How do you think it worked out?


Hope you enjoyed this guide, stay tuned for more.

💖 💪 🙅 🚩
murashow
Makar Murashov

Posted on July 9, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related