👀

ResizeObserverを用いて要素のサイズ変更時に再レンダリングさせる

2023/01/23に公開約2,600字

はじめに

例えば、「APIからのレスポンスをラジオボタンで表現したいがブラウザの幅に合わせて一定を超えるとプルダウンにしたい」などといった時に要素のリサイズを検知したくなります。

ReactではDOMにアタッチしたい場合useRefというAPIを用いることが多いと思います。しかし、useRefの返り値はプレーンなJavaScriptのオブジェクトであり、変更があってもコンポーネントのライフサイクルには影響しません。そのため、要素のサイズ変更を知りたい場合にはuseEffectと一緒にwindow.addEventListener('resize', () => {});などと書かなければいけません。しかしこれではブラウザのリサイズには対応できるものの、サイドバーの開閉時のような時には対応できません。

これはResizeObserverというWebAPIを用いて要素を監視することにより簡潔かつ簡単に解決することができます。

ResizeObserver

https://developer.mozilla.org/ja/docs/Web/API/ResizeObserver

コンストラクタに渡されたコールバック関数を、observeにより購読した要素のサイズ変更時に呼び出すようになっています。コールバック関数の引数にはcontentRectというheightwidthを参照できるプロパティを持った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を用いることでより宣言的に書くことができます。
https://beta.reactjs.org/reference/react/useSyncExternalStore

本記事ではuseSyncExternalStoreの詳細には触れません。詳しい解説はこちらの記事が参考になります。
https://zenn.dev/stin/articles/use-sync-external-store-with-match-media

デモ

Discussion

ログインするとコメントできます