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)
}
});
- 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
- 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;
}
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:
- It should be universal and its usage must be as simple as
const size = giveMeMySize()
; - 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; - In order make the hook handy to use it should deal with mutations itself and automatically stop observing on unmount;
- 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 aResizeObserver
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
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]
}
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) });
};
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]);
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 });
});
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)
What if we should change the target element dynamically?
Олексій, 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 makemyElementRef
to be dynamic inside your component.