useLayoutEffect は、より早く実行される useEffect です。ReactがDOMを更新した直後、ブラウザが新しいフレームを描画する前に実行されます。その中のコードは要素を測ってレイアウトを変えられ、ユーザーが目にするのは最終的な結果だけです。実行中は描画を止めるので、そのタイミングが必要なときだけ使ってください。
エフェクトはエフェクト、レイアウトエフェクトの順で宣言されていますが、コンソールにはレンダリングのたびにレイアウトエフェクトが先に表示されます。この2つのフックの間では宣言の順番は関係なく、タイミングがすべてです。
構文
シグネチャは useEffect とまったく同じです。セットアップ関数、省略可能なクリーンアップ、依存配列です。
useLayoutEffect(() => {
// runs after the DOM update, before paint
return () => {
// cleanup, before the next run and on unmount
};
}, [dependencies]);
useEffectで学んだ依存値とクリーンアップのことはすべて当てはまります。違うのは、Reactがいつそれを呼ぶかだけです。
それぞれが実行されるタイミング
レンダリングの後、ReactはDOMを更新し、決まった順番で3つのことを行います。
- すべてのレイアウトエフェクトのクリーンアップとレイアウトエフェクトを、同期的に実行します。
- ブラウザに描画させます。レイアウトエフェクトがstateをセットしていたら、Reactはその前に、まだ描画前のうちにもう一度レンダリングします。
- 通常のエフェクトを、たいていは描画の後に実行します。
ツリー全体では、すべてのレイアウトエフェクトがどの通常のエフェクトよりも先に実行され、子は親より先に実行されます。親が完了できるのは、子が所定の位置に収まってからだからです。
コンソールには、まず2つのレイアウトエフェクトがChild、Parentの順で表示され、次に2つの通常のエフェクトが同じ順で表示されます。key={n} によってクリックのたびに新しい Parent がマウントされるので、毎回同じ4行が出ます。各エフェクトにログを出すクリーンアップを追加すると、古いものが新しいものより先に実行される様子が見られます。
ちらつきの問題
ボタンの上に表示されるべきツールチップを考えてください。ただし上に余裕がなければ下に表示します。余裕があるかどうかは、ツールチップがDOMに入って高さを測れるようになるまでわかりません。そのためレンダリングは2回に分かれます。
- 測れるように、ツールチップをどこかに描画します。
- それを測り、本当の位置を計算し、stateをセットして、もう一度レンダリングします。
useEffect だと、その2回の間にブラウザが描画することがあり、ユーザーにはツールチップが最初の位置から2つ目の位置へ飛ぶのが見えます。useLayoutEffect なら2回目が描画の前に起きるので、最初の位置は決して表示されません。
上のボタンにホバーしてください。上に余裕がないので、ツールチップは下に表示されます。下のボタンにホバーすると上に表示されます。測った結果どちらの側を選んだかは、コンソールでわかります。useLayoutEffect を useEffect に変えてもう一度ホバーしてみてください。今度はツールチップが、移動する前に1フレームだけボックスの上端で光ることがあります。速いマシンでは気づかないほど一瞬のことが多く、だからこそこのバグはリリースされてしまい、遅いスマートフォンから報告されるのです。
offsetHeight、offsetTop、getBoundingClientRect() はどれもブラウザにレイアウトの計算を強制するので、レイアウトエフェクトの中で読めば、Reactが今書いたDOMの本当の値が得られます。
要素を測る
同じパターンは、描画が依存するどんなサイズにも使えます。レイアウトエフェクトでrefを通じて読み、stateに保存し、それを使って描画します。
ボタンをクリックすると、幅の行がテキストと同じフレームで更新されます。2回目のレンダリングが描画の前に起きるので、ページに新しいテキストと古い幅が並んで表示されることはありません。依存配列から [text] を取り除くと、自分の setWidth が起こすレンダリングも含め、レンダリングのたびに計測が実行されます。そこで止まるのは、幅がもう変わらないからにすぎません。
useEffectを優先する
レイアウトエフェクトは、それが戻るまで、stateをセットするなら追加のレンダリングが終わるまで、描画を止めます。遅いレイアウトエフェクトは、ページのあらゆる更新を遅く感じさせます。useLayoutEffect は、次のすべてが当てはまるときだけ使ってください。
- エフェクトがレイアウト(サイズ、位置、スクロール)を読む。
- その読み取りに基づいて描画内容を変える。
- 途中の状態が目に見え、おかしく見える。
それ以外(データ取得、購読、タイマー、ログ、分析)はすべて useEffect に書きます。手早い目安は、useEffect から始めて、ちらつきが見えたときだけ切り替えることです。
useEffect は通常は描画の後に実行されますが、いつもそうとは限りません。クリックやキー入力のような個別のユーザー操作から更新が来た場合、Reactはブラウザが描画する前にエフェクトを実行することがあります。ちらつきがある状況では現れ、別の状況では現れない理由の1つがこれです。
サーバーレンダリング
サーバーレンダリング中にエフェクトは実行されず、useLayoutEffect も同じです。サーバーは最初のレンダリングのHTMLを送り、レイアウトエフェクトはブラウザでのハイドレーションの後にだけ実行されます。React 18以前は、そのようなコンポーネントがサーバーで描画されると「useLayoutEffect does nothing on the server」と警告していましたが、React 19ではその警告はなくなりました。
計測なしではサーバーで描画した最初のフレームがおかしく見えるなら、よくある直し方は2つあります。中立的なフォールバックを描画する(たとえば測るまでツールチップを隠す)か、コンポーネントをクライアントでだけ描画するかです。
function ClientOnly({ children }) {
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
return mounted ? children : null;
}
useInsertionEffect
3つ目のエフェクトのフック useInsertionEffect があり、どのレイアウトエフェクトよりも先に実行されます。実行時に <style> タグを生成するCSS-in-JSライブラリのためのものです。そこでスタイルを挿入しておけば、要素を測るレイアウトエフェクトは、すでにスタイルが付いた状態を見られます。stateを更新できず、refもまだ付いていないので、アプリケーションのコードでは役に立ちません。スタイリングのライブラリを書いているのでなければ、必要になることはないでしょう。
よくある質問
useEffectとuseLayoutEffectの違いは何ですか?
タイミングです。useLayoutEffect は、ReactがDOMを変えた後、ブラウザが描画する前に同期的に実行されるので、その中でのstateの更新はユーザーが何かを見る前に適用されます。useEffect は通常、描画の後に実行されます。APIと依存配列は同じです。
useLayoutEffectはいつ使うべきですか?
エフェクトがレイアウト(サイズや位置)を読み、それに基づいてすぐに描画内容を変えるときです。たとえばツールチップをボタンの上か下に配置する場合です。これを使わないと、ユーザーには最初の間違った位置が1フレームだけ見えることがあります。
useLayoutEffectはパフォーマンスに悪いですか?
終わるまで、それが起こす再レンダリングも含めて、ブラウザの描画を止めます。遅いレイアウトエフェクトはページ全体を遅く感じさせるので、目に見えるちらつきを避ける必要がない限り useEffect を使ってください。
useLayoutEffectはサーバーで実行されますか?
いいえ。サーバーレンダリング中はどちらのエフェクトも実行されません。古いバージョンのReactは、useLayoutEffect を持つコンポーネントがサーバーで描画されると警告を出していました。React 19ではもう出しませんが、エフェクトが実行されるのはやはりブラウザの中だけです。