ResizeObserverを用いて要素のサイズ変更時に再レンダリングさせる
はじめに
例えば、「APIからのレスポンスをラジオボタンで表現したいがブラウザの幅に合わせて一定を超えるとプルダウンにしたい」などといった時に要素のリサイズを検知したくなります。
ReactではDOMにアタッチしたい場合useRef
というAPIを用いるかと思います。しかし、useRef
の返り値はプレーンなJavaScriptのオブジェクトであり、変更があってもコンポーネントのライフサイクルには影響しません。そのため、要素のサイズ変更を知りたい場合にはuseEffect
と一緒にwindow.addEventListener('resize', () => {});
などと書かなければいけません。しかしこれではブラウザのリサイズには対応できるものの、サイドバーの開閉時のような時には対応できません。
これはResizeObserver
というWebAPIを用いて要素を監視することにより簡潔かつ簡単に解決することができます。
ResizeObserver
コンストラクタに渡されたコールバック関数を、observe
により購読した要素のサイズ変更時に呼び出すようになっています。コールバック関数の引数にはcontentRect
というheight
やwidth
を参照できるプロパティを持ったResizeObserverEntry
の配列が渡ってくるので、それらを見ることによってサイズを取得します。
インタフェースは以下のようになっています。
interface ResizeObserver {
disconnect(): void;
observe(target: Element, options?: ResizeObserverOptions): void;
unobserve(target: Element): void;
}
declare var ResizeObserver: {
prototype: ResizeObserver;
new(callback: ResizeObserverCallback): ResizeObserver;
};
interface ResizeObserverCallback {
(entries: ResizeObserverEntry[], observer: ResizeObserver): void;
}
interface ResizeObserverEntry {
readonly borderBoxSize: ReadonlyArray<ResizeObserverSize>;
readonly contentBoxSize: ReadonlyArray<ResizeObserverSize>;
readonly contentRect: DOMRectReadOnly;
readonly devicePixelContentBoxSize: ReadonlyArray<ResizeObserverSize>;
readonly target: Element;
}
実装
useRef
の返り値をdivタグに渡し、ResizeObserver
に購読させます。
再レンダリングしたいため、サイズを格納するuseState
を定義し、ResizeObserverに渡すコールバック関数内でsetterを呼びます。要素がリサイズされたタイミングでコールバック関数が呼び出され、stateが更新されることにより再レンダリングが発生します。
import * as React from 'react';
export default function App() {
const [width, setWidth] = React.useState(0);
const ref = React.useRef<HTMLDivElement | null>(null);
React.useEffect(() => {
const observer = new ResizeObserver((entries) => {
entries.forEach((el) => {
// ここで要素を取得できる
setWidth(el.contentRect.width);
});
});
if (ref.current) {
observer.observe(ref.current);
}
return () => {
observer.disconnect();
};
}, []);
return <div ref={ref}>{width}</div>;
}
デモ
ウィンドウ幅を変えると、再レンダリングされ数字が変わっているのがわかります。
React18では
useSyncExternalStore
を用いることでより宣言的に書くことができます。
本記事ではuseSyncExternalStore
の詳細には触れません。詳しい解説はこちらの記事が参考になります。
Discussion
ありがとうございました。とても勉強になりました!