DEV Community 👩‍💻👨‍💻

Cover image for How to use Resize Observer with React
Makar Murashov
Makar Murashov

Posted on

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.

Top comments (2)

Collapse
 
alexiuscrowua profile image
Олексій Сапон

What if we should change the target element dynamically?

Collapse
 
murashow profile image
Makar Murashov Author • Edited on

Олексій, I suppose then this logic should live inside your component with dynamic targets. You can choose where you want to put targetRef from hook depending on something <MyElement ref={isSomething ? tagetRef : undefined}.
Other option is to slightly tweak the hook, so it will accepts the target ref as a prop, i.e. usage would be const {width, height} = useElementSize(myElementRef). But still you have to make myElementRef to be dynamic inside your component.

🤔 Did you know?

 
🌚 You can turn on dark mode in Settings