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 計測用です。ユーザーに最初の画面を早く見せるため、必要な視覚処理にだけ使います。