All Articles

Reactで要素のリサイズを検知する

2020/04/11 追記
Safariもバージョン13.1ResizeObserverに対応するみたいです。
これにより、主要なブラウザはResizeObserverに対応することとなります。

まだSafari13.1はベータリリース状態ではあるものの、
今後はResizeObserverを使うのが主流になっていきそうですね。
(ターゲットユーザー・動作環境との兼ね合いは考える必要がありますが)。

動機

Webフロントエンド開発で、たまに要素のリサイズを検知したいことがあります。
たとえば以下のようなレイアウトがあったとします。

レイアウトのイメージ

ここで、左のメニューが閉じる場合に右側の要素(背景グレーの領域)でリサイズが発生するとします。
通常、レスポンシブなスタイルを適応していれば特に問題は無いと思いますが、都合により右側の領域の寸法をJavaScriptで取得して扱いたい場合があります(私はありました)。

そうなると右側領域の要素でresizeイベントを扱いたくなりますが、 Elementオブジェクトではresizeイベントを使うことはできません。

// 右側領域要素を取得
const rightPane = document.getElementById('right-pane');

// ※ そんなものはないため呼ばれない
rightPane.addEventListener('resize', () => {
    console.log('要素のリサイズです');
});

// ※ これはある
window.addEventListener('resize', () => {
    console.log('windowのリサイズです');
});

解決の手口を掴もうと足掻いていたところ、以下のような記事を見つけました。

Cross-Browser, Event-based, Element Resize Detection - Back Alley Coder

なるほど。

JavaScriptでやるならば上記記事を参考にする、もしくは上記記事の手法を実装したライブラリなどを使うのが良さそうです。
今回は上記記事の内容を参考にReactで要素のリサイズを検出してみたりしましたので、備忘録としてまとめておきます。

ゴールと方針

ゴールは、「Reactを用いてブラウザサイズが変わらない場合の要素のリサイズを検出する」ことです。取る方針は以下のとおりです。

<object>のような、内部にWindowオブジェクトを持ちうる要素を、検出対象となる要素の内側に配置します。
この時、対象の要素のスタイルでposition: relativeとし、<object>position: absoluteで幅いっぱいに広がるようにします。
あとは<object>内部のWindowオブジェクトにリサイズイベントのコールバックを設定してあげます。

実装 - 非表示のobject要素を使う

サンプルの実装は以下になります。

interface Props {
  onResize: (event: Event) => void;
};

const ElementResizeListener: React.FC<Props> = ({ onResize }) => {
  const rafRef = useRef(0);
  const objectRef: RefObject<HTMLObjectElement> = useRef(null);
  const onResizeRef = useRef(onResize);

  onResizeRef.current = onResize;

  const _onResize = useCallback((e: Event) => {
    if (rafRef.current) {
      cancelAnimationFrame(rafRef.current);
    }
    rafRef.current = requestAnimationFrame(() => {
      onResizeRef.current(e);
    });
  }, []);

  const onLoad = useCallback(() => {
    const obj = objectRef.current;
    if (obj && obj.contentDocument && obj.contentDocument.defaultView) {
      // defaultViewはDocumentに関連付けられているWindowオブジェクトを返す
      // object要素のWindowオブジェクトに対し、リサイズイベントを登録する
      obj.contentDocument.defaultView.addEventListener('resize', _onResize);
    }
  }, []);

  useEffect(() => {
    // クリーンアップ処理
    return () => {
      const obj = objectRef.current;
      if (obj && obj.contentDocument && obj.contentDocument.defaultView) {
        obj.contentDocument.defaultView.removeEventListener('resize', _onResize);
      }
    }
  }, []);

  return (
    <object
      onLoad={onLoad}
      ref={objectRef} tabIndex={-1}
      type={'text/html'}
      data={'about:blank'}
      title={''}
      style={{
        position: 'absolute',
        top: 0,
        left: 0,
        height: '100%',
        width: '100%',
        pointerEvents: 'none',
        zIndex: -1,
        opacity: 0,
      }}
    />
  )
}

export default ElementResizeListener;

この例では<object>resizeイベントコールバックをpropsから渡せるような形にしていますが、<object>の寸法をstateに持たせても良いかと思います。

他の方法

1. resizeイベント

Window: resize event - Web APIs | MDN

Windowオブジェクトでのみ発火しうるイベントです。

Windowオブジェクトのリサイズを検知するだけで十分な場合にはこれで十分ですが、
(たとえば、左メニューは固定で右側領域のサイズを取得したい場合)要素同士でのサイズ干渉がありそれを検知したい場合にはwindow.onresizeに頼ることはできません。

普通に考えてWindow以外の要素オブジェクトでもresizeイベントがあって然るべきな気もするのですが、もともとが「viewportwidthまたはheightに変化があった際に、Windowオブジェクトでresizeイベントを発火する」というニュアンスみたいです。

https://www.w3.org/TR/cssom-view-1/#resizing-viewports

また、W3Cの定義でviewportwidthまたはheightが変わる場合の例として、

e.g. as a result of the user resizing the browser Window, or changing the page zoom scale factor, or an iframe element’s dimensions are changed

としています。
おおまかに和訳すると

  1. ユーザーがブラウザウィンドウの大きさを変えたとき
  2. ページの表示倍率を変えたとき
  3. iframe要素の寸法が変わった時

とされています。
(例を挙げているだけなのでこれらで全てというわけではない可能性があります)。
今回紹介する方法では3.も関係してくるので頭の片隅で覚えておきたいところです。

2. イベントを用いず、定期的にサイズを取得する

setInterval()など用いて対象となる要素のサイズを都度取得するイメージです。要素のサイズ変更を即座に反映しなくても良い場合にはこれでも良いかもしれません。

ただし、サイズに変更がない場合にも監視のための処理が走りますから、取得頻度を上げた場合にはパフォーマンスに影響が出るかもしれません。

3. ResizeObserver

ResizeObserver - Web API | MDN

あるじゃないですか!

しかし、残念ながらクロスブラウザ対応の点からはまだまだ厳しそうです。
MDNを見た所、本記事執筆時点での対応ブラウザはChromeやOperaなど一部のブラウザのみで、SafariやFirefox、Edgeでは未対応です。

[2020/04/11] 主要なブラウザでの実装はほぼ済んでいます。必要に応じて各種ブラウザの対応状況を確認しつつResizeObserverを使うのが良いと思います。

本格的にブラウザの実装が進めばこの記事に書いている技を使うまでもなくなるはずです。

ResizeObserverについてはこの記事では仔細を語りません。存在を仄めかす程度に留めておきます。

まとめ

<object>などの要素を用いてWindowオブジェクトのresizeイベントを扱うのは盲点でした。
例の記事に感謝です。
ResizeObserverが扱えれば不要になる手段かもしれませんが、しばらくはこの方法を使うことになりそうです。

ソースコードはGithubに上げてあるのでもしよければ参考にしてみてください。
GitHub

参考文献