useLayoutEffect

useLayoutEffect を使うべき描画前の DOM 計測と、描画をブロックする注意点を学ぶ。

useLayoutEffect は、React が DOM を更新した後、ブラウザが画面を再描画する前に実行される Effect Hook です。要素の寸法を測り、その結果を使って表示位置を決めるときに使います。

描画前に計測する

ツールチップの高さを測り、上に収まらなければ対象の下へ表示する例です。targetRect は対象要素の画面内の位置です。

import { useLayoutEffect, useRef, useState } from 'react';

type TargetRect = { top: number; bottom: number; left: number };

function Tooltip({ targetRect }: { targetRect: TargetRect }) {
  const ref = useRef<HTMLDivElement>(null);
  const [height, setHeight] = useState(0);

  useLayoutEffect(() => {
    setHeight(ref.current?.getBoundingClientRect().height ?? 0);
  }, []);

  const top = targetRect.top - height >= 0
    ? targetRect.top - height
    : targetRect.bottom;

  return <div ref={ref} role="tooltip" style={{ position: 'fixed', top, left: targetRect.left }}>説明</div>;
}

height が 0 の最初のレンダーでは DOM を置くだけです。layout effect が実寸を読み、setHeight で再レンダーを予約します。計測後は対象の上に収まるかを判定し、収まらなければ下へ置きます。この例は上端だけを判定しており、画面下端や横幅の不足まで扱うツールチップには追加の計測が必要です。

クライアントで初めて表示する場合は、ブラウザが描画する前に計測と再レンダーを終えられるため、位置が一瞬ずれるちらつきを防げます。

処理の順序は、おおまかに「React が DOM を反映する → useLayoutEffect で計測・更新する → ブラウザが描画する」です。通常の useEffect は描画後に実行されるため、初期位置と計測後の位置が別フレームで見えることがあります。

まず useEffect を選ぶ

useLayoutEffect とその中で起きる state 更新は描画を待たせます。データ同期、イベント購読、タイトル変更など、見た目を測定しない処理には通常の useEffect を使います。視覚的な不具合を確認してから置き換えるのが安全です。

落とし穴

サーバーではレイアウト情報を取得できません。SSR した HTML は JavaScript の読み込みと hydration より前に表示され得るため、この Hook だけでは初期表示のちらつきを防げません。レイアウト計測が必須の UI は、クライアントでだけ表示するか、hydration 前には fallback を表示します。依存配列と cleanup の規則は useEffect と同じです。

計測結果の state 更新が連鎖すると、描画を長く止めたり無限ループになったりします。計測値が前回と同じなら更新しない、依存配列へ正しい値だけを入れる、といった基本を守ります。ResizeObserver の購読が必要な場合も cleanup で必ず解除します。

よくある使いどころ

ツールチップ、ポップオーバー、ドラッグ中の位置補正、仮想化リストの寸法計測が代表例です。画像の読み込みやフォントの反映後にサイズが変わる場合は、一度の計測では足りません。その場合も「いつ再計測するか」を明示し、すべての再レンダーで無条件に計測しないようにします。

まとめ

useLayoutEffect は描画前の DOM 計測用です。ユーザーに最初の画面を早く見せるため、必要な視覚処理にだけ使います。

Sources