React で副作用を扱うときは useEffect を使うのが基本ですが、まれに「画面に表示される前にDOMを測って調整したい」という場面があります。そこで登場するのが useLayoutEffect です。useEffect とほぼ同じ書き方でありながら、実行されるタイミングだけが異なります。この記事では、useLayoutEffect の基本的な使い方、useEffect との実行タイミングの違い、そして実際にちらつきを防ぎたい場面での使いどころと、選ぶときの指針まで、初心者〜中級者向けに解説します。
目次
useLayoutEffect と useEffect の違い
どちらも「レンダリング後に副作用を実行する」フックですが、実行されるタイミングが違います。React がDOMを更新したあと、ブラウザが実際に画面を描画する前に同期的に実行されるのが useLayoutEffect、描画されたあとに実行されるのが useEffect です。処理の流れを表で比べます。
| 観点 | useEffect | useLayoutEffect |
|---|---|---|
| 実行タイミング | 画面が描画された後 | DOM更新後・描画前 |
| 実行のされ方 | 非同期(描画をブロックしない) | 同期(描画をブロックする) |
| DOM測定の結果反映 | 一瞬ちらつくことがある | ちらつかない |
| 基本的な使いどころ | データ取得・購読など大半の副作用 | レイアウト測定と即時の見た目調整 |
useLayoutEffect は描画前に同期的に走るため、そこでDOMのスタイルを変更しても、ユーザーは変更前の状態を目にしません。一方 useEffect は描画後に走るので、いったん表示されてから調整されると、その差が一瞬のちらつきとして見えることがあります。この違いが唯一にして最大のポイントです。
基本の書き方
書き方は useEffect とまったく同じです。第1引数に副作用の関数、第2引数に依存配列を渡します。関数からクリーンアップ関数を返せる点も共通です。
import { useLayoutEffect, useRef } from "react";
function Box() {
const ref = useRef<HTMLDivElement>(null);
useLayoutEffect(() => {
if (ref.current) {
// 描画される前に要素の高さを測れる
const height = ref.current.getBoundingClientRect().height;
console.log("高さ:", height);
}
}, []); // 依存配列は useEffect と同じルール
return <div ref={ref}>測定対象</div>;
}
useRef で対象の要素を参照し、useLayoutEffect の中で getBoundingClientRect() を使って高さを測っています。このタイミングではすでにDOMは更新済みなので正しいサイズが取れ、かつまだ描画前なので、ここでスタイルを変えても画面のちらつきになりません。
ちらつきが問題になる例
典型的なのが、要素のサイズや位置を測ってから、その結果に応じて表示位置を決めるケースです。たとえばツールチップを対象要素の真上に出したい場合、ツールチップの高さを測ってから位置を計算する必要があります。この「測って→位置を反映する」流れを useEffect でやると、いったん仮の位置で描画されてから正しい位置に飛ぶため、ちらつきとして見えることがあります。
import { useLayoutEffect, useRef, useState } from "react";
function Tooltip({ text }: { text: string }) {
const ref = useRef<HTMLDivElement>(null);
const [top, setTop] = useState(0);
useLayoutEffect(() => {
if (ref.current) {
// ツールチップ自身の高さぶん上にずらす
const { height } = ref.current.getBoundingClientRect();
setTop(-height - 8);
}
}, [text]);
return (
<div ref={ref} style={{ position: "absolute", top }}>
{text}
</div>
);
}
ツールチップの高さを測り、その値をもとに top を決めています。useLayoutEffect の中で setTop を呼ぶと、React は描画前に再レンダリングを行い、位置が確定した状態で初めて画面に表示されます。そのため、ユーザーには最初から正しい位置のツールチップだけが見え、位置がずれてから直る様子は見えません。同じコードを useEffect にすると、仮位置で一度描画されてから位置が変わるため、環境によっては一瞬のちらつきが出ます。
基本は useEffect、必要なときだけ useLayoutEffect
useLayoutEffect は描画をブロックして同期的に実行されます。つまり、中の処理が重いとその分だけ画面表示が遅れます。便利だからと何でもこれに置き換えると、かえって表示が遅くなることがあります。使い分けの基本方針は次のとおりです。
- データ取得、イベントの購読、ログ送信など大半の副作用は
useEffectを使う - DOMのサイズ・位置を測って、その結果を描画前に見た目へ反映したいときだけ
useLayoutEffectを使う - 迷ったらまず
useEffectで書き、ちらつきが気になったときにuseLayoutEffectへ切り替える
ほとんどのケースは useEffect で問題ありません。useLayoutEffect は「描画前に測って直す」という限られた目的のための道具だと考えると、使いどころを見誤りにくくなります。
サーバーサイドレンダリングでの警告
サーバーサイドレンダリング(SSR)を行う環境で useLayoutEffect を使うと、コンソールに警告が表示されることがあります。これは、サーバー側にはレイアウトを行うDOMが存在せず、useLayoutEffect が実行されないことに起因します。
対処としては、レイアウト測定が本当に必要かを見直し、不要なら useEffect に変えるのが基本です。どうしても描画前の測定が必要な場合は、その処理をクライアント側でのみ動かすよう、マウント後に実行される仕組みと組み合わせて設計します。SSR 環境では「サーバーとクライアントで見た目が一致しているか」も意識し、初期表示に影響する測定処理はできるだけ避けるのが安全です。
まとめ
useLayoutEffect は、DOMの更新後・ブラウザの描画前に同期的に副作用を実行するフックです。書き方は useEffect と同じで、違うのは実行タイミングだけです。getBoundingClientRect() などで要素のサイズや位置を測り、その結果を描画前に見た目へ反映したい場面では、useEffect で起きがちな一瞬のちらつきを防げます。ただし描画をブロックするため、大半の副作用は useEffect を使い、レイアウト測定と即時反映が必要なときだけ useLayoutEffect を選ぶのが基本です。SSR 環境では警告が出ることも踏まえ、用途を絞って活用してみてください。