- React
React useLayoutEffectとuseEffectの違いと実行順
useLayoutEffectとuseEffectの違いは、実行されるタイミングです。useLayoutEffectはDOMの更新が終わった直後、ブラウザが画面を描く前に実行されます。useEffectは、ふつうはブラウザが画面を描いたあとに実行されます。 そのため、「要素のサイズや位置を測って、その結果で表示を変える」処理をuseEffectに書くと、変える前の状態が一瞬表示されることがあります。これが「ちらつき」です。
この記事では、レンダー → useInsertionEffect → useLayoutEffect → ブラウザの描画 → useEffect の順番を、親子コンポーネントの console.log とクリーンアップの順序まで含めて、実際のログで示します。 さらに、ツールチップの位置計測を題材に、ずれた位置が何フレーム表示されるかを数え、useLayoutEffectに変えると0フレームになることを確かめます。サーバー側(renderToString)での動作と、その場合の書き方も実行結果つきで載せます。
確認環境:Ubuntu 24.04、Node.js 22.22.2、React 19.3.0 / react-dom 19.3.0(サーバー側の比較用に React 18.3.1)。esbuild 0.28.2でまとめ、Playwright 1.56.0のChromium 141で開発ビルドを動かしました。
useEffect・useLayoutEffect・useInsertionEffectの早見表
| 項目 | useInsertionEffect | useLayoutEffect | useEffect |
|---|---|---|---|
| 実行タイミング | useLayoutEffectより前 | DOM更新の直後、描画の前 | ふつうは描画のあと |
| ブラウザの描画を待たせるか | 待たせる | 待たせる(中のstate更新による再レンダーも含む) | 待たせない |
| refが付いているか(今回の計測) | 付いていない(null) |
付いている | 付いている |
| 中でstateを更新できるか | できない(公式ドキュメントの記載) | できる。描画前に再レンダーされる | できる |
| 主な用途 | CSS-in-JSライブラリがstyleタグを差し込む | サイズ・位置の計測、スクロール位置の調整 | データ取得、購読、タイマー、ログ送信 |
サーバーでの動作(renderToString) |
実行されない | 実行されない | 実行されない |
| 選ぶ目安 | ライブラリを作る人向け | 計測結果で見た目を変えるとき | それ以外のすべて |
| 知りたいこと | 答え |
|---|---|
| 実行順は? | レンダー(親→子)→ useInsertionEffect(子→親)→ useLayoutEffect(子→親)→ 描画 → useEffect(子→親) |
| 迷ったら? | useEffect。ちらつきが見えたときだけuseLayoutEffectに変える |
| 書き方の違いは? | フックの名前だけ。引数(関数と依存配列)とクリーンアップの書き方は同じ |
サーバーで window を読むには? |
レンダー中は読めない。エフェクトの中で読むか、マウント後に描画する |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「React useLayoutEffectとuseEffectの違いと実行順」
実行順をログで確かめる:親子コンポーネントの場合
3つのフックすべてにログを仕込んだカスタムフック useLog を作り、親と子の両方で使います。ブラウザの描画の目印には requestAnimationFrame を使いました。これは「次の描画の直前」に呼ばれるブラウザのAPIです。
// b1.jsx(React 19.3.0)
import { useEffect, useLayoutEffect, useInsertionEffect, useState } from 'react';
import { createRoot } from 'react-dom/client';
function useLog(name, n) {
console.log(`1. render ${name}`);
useInsertionEffect(() => {
console.log(`2. useInsertionEffect ${name}`);
return () => console.log(` cleanup(insertion) ${name}`);
}, [n]);
useLayoutEffect(() => {
console.log(`3. useLayoutEffect ${name}`);
return () => console.log(` cleanup(layout) ${name}`);
}, [n]);
useEffect(() => {
console.log(`5. useEffect ${name}`);
return () => console.log(` cleanup(effect) ${name}`);
}, [n]);
}
function Child({ n }) {
useLog('Child', n);
return <p>count: {n}</p>;
}
function Parent() {
const [n, setN] = useState(0);
useLog('Parent', n);
// ブラウザの描画の目印:描画の直前に呼ばれる requestAnimationFrame
useLayoutEffect(() => {
requestAnimationFrame(() => console.log('4. ---- ブラウザの描画(requestAnimationFrame)----'));
}, [n]);
window.update = () => setN((v) => v + 1);
return <Child n={n} />;
}
const root = createRoot(document.getElementById('root'));
console.log('=== マウント ===');
root.render(<Parent />);
setTimeout(() => { console.log('=== 更新(setN)==='); window.update(); }, 200);
setTimeout(() => { console.log('=== アンマウント ==='); root.unmount(); }, 400);
マウント時の順番
=== マウント ===
1. render Parent
1. render Child
2. useInsertionEffect Child
2. useInsertionEffect Parent
3. useLayoutEffect Child
3. useLayoutEffect Parent
4. ---- ブラウザの描画(requestAnimationFrame)----
5. useEffect Child
5. useEffect Parent
- レンダーは親から子へ、エフェクトは子から親への順で進みます。
- useLayoutEffectは描画の前、useEffectは描画のあとに実行されています。
- 同じ種類のエフェクトは、子と親の分がまとめて実行されます(useLayoutEffectが全部終わってから描画、そのあとuseEffectが全部)。
更新時の順番(クリーンアップを含む)
=== 更新(setN)===
1. render Parent
1. render Child
cleanup(insertion) Child
2. useInsertionEffect Child
cleanup(layout) Child
cleanup(insertion) Parent
2. useInsertionEffect Parent
cleanup(layout) Parent
3. useLayoutEffect Child
3. useLayoutEffect Parent
4. ---- ブラウザの描画(requestAnimationFrame)----
cleanup(effect) Child
cleanup(effect) Parent
5. useEffect Child
5. useEffect Parent
更新時は、前回のエフェクトのクリーンアップが先に呼ばれ、そのあと新しいエフェクトが実行されます。種類ごとに少し違いがあります。
| フック | クリーンアップと実行の並び方 |
|---|---|
| useInsertionEffect | コンポーネント1つごとに「クリーンアップ → 実行」を続けて行う(Childの片付けと実行、次にParentの片付けと実行) |
| useLayoutEffect | 全コンポーネントのクリーンアップが済んでから、全コンポーネントの実行 |
| useEffect | 描画のあとに、全コンポーネントのクリーンアップ → 全コンポーネントの実行 |
useLayoutEffectのクリーンアップは描画の前、useEffectのクリーンアップは描画のあとに呼ばれます。 useInsertionEffectだけ「1コンポーネントずつ交互に」進む点は、公式リファレンスの注意事項にも書かれています。
アンマウント時の順番
=== アンマウント ===
cleanup(insertion) Parent
cleanup(layout) Parent
cleanup(insertion) Child
cleanup(layout) Child
cleanup(effect) Parent
cleanup(effect) Child
アンマウント時のクリーンアップは、親から子への順です。useInsertionEffectとuseLayoutEffectのクリーンアップが先に済み、useEffectのクリーンアップが最後になります。
全体の流れ
| 順番 | 段階 | 起きること |
|---|---|---|
| 1 | レンダー | コンポーネントの関数が呼ばれ、JSXが返る(親→子) |
| 2 | useInsertionEffect | styleタグを差し込むための場所(子→親) |
| 3 | refの設定 | ref.current に要素が入る |
| 4 | useLayoutEffect | 更新後のDOMを読める。ここでstateを更新すると、描画の前に再レンダーされる |
| 5 | ブラウザの描画 | 画面に表示される |
| 6 | useEffect | 描画を待たせずに実行される |
ツールチップの位置計測でちらつきを再現する
useLayoutEffectが必要になる代表的な場面が、「要素を一度描いてサイズを測り、その結果で位置を決め直す」処理です。ボタンの上に出すツールチップで考えます。ツールチップの高さは中身の文章で変わるため、描いてみるまでわかりません。そこで、最初は高さ0として描き、エフェクトの中で高さを測ってstateに入れ、正しい位置に置き直します。
次のコードは、エフェクトのフックをpropsで差し替えられるようにして、同じ画面に「useEffect版」と「useLayoutEffect版」を並べたものです。2つの違いは、使うフックの名前だけです。
// b2.jsx(React 19.3.0)
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
import { createRoot } from 'react-dom/client';
// ボタンの上に出すツールチップ。自分の高さを測ってから位置を決める
function Tooltip({ targetRect, useHook, children }) {
const ref = useRef(null);
const [height, setHeight] = useState(0); // 最初は高さがわからないので 0
useHook(() => {
setHeight(ref.current.getBoundingClientRect().height);
}, []);
// 確認用:レンダーに時間がかかる状況を作る(window.SLOW ミリ秒だけ待つ)
if (window.SLOW) { const t = performance.now(); while (performance.now() - t < window.SLOW) {} }
const top = targetRect.top - height - 8; // ボタンの上端から、高さ+8px だけ上
return (
<div id="tip" ref={ref}
style={{ position: 'fixed', left: targetRect.left, top, padding: 8, background: '#333', color: '#fff' }}>
{children}
</div>
);
}
function Demo({ useHook, label }) {
const btn = useRef(null);
const [rect, setRect] = useState(null);
const show = () => setRect(btn.current.getBoundingClientRect());
return (
<div style={{ display: 'inline-block', margin: '200px 40px 0' }}>
<button id={label} ref={btn} onMouseEnter={show} onMouseLeave={() => setRect(null)}>
{label}
</button>
{rect && <Tooltip targetRect={rect} useHook={useHook}>説明文<br />2行目</Tooltip>}
</div>
);
}
// フレームごと(requestAnimationFrame ごと)にツールチップの top を記録する
window.startSampling = () => {
window.frames = [];
let t0 = null;
const tick = (now) => {
const el = document.getElementById('tip');
if (el) {
if (t0 === null) t0 = now;
window.frames.push([Math.round(now - t0), Math.round(el.getBoundingClientRect().top)]);
}
if (window.frames.length < 30) requestAnimationFrame(tick);
};
requestAnimationFrame(tick);
};
createRoot(document.getElementById('root')).render(
<>
<Demo useHook={useEffect} label="effect" />
<Demo useHook={useLayoutEffect} label="layout" />
</>
);
ボタンの上端は top = 200、ツールチップの高さは58pxでした。正しい位置は 200 - 58 - 8 = 134、高さ0のまま描いたときの位置は 200 - 0 - 8 = 192 です。Playwrightでボタンにマウスを乗せ、ツールチップが現れてから30フレーム分の top を記録して、192だったフレームの数を数えました。
レンダーが軽いとき(待ち時間なし)
ボタンの上端 top = 200 (正しい位置は 192 - ツールチップの高さ)
effect 記録 30 フレーム / ずれた位置(top=192)のフレーム: 0 / 正しい位置(top=134)になった時刻: 0ms
layout 記録 30 フレーム / ずれた位置(top=192)のフレーム: 0 / 正しい位置(top=134)になった時刻: 0ms
レンダーに30ミリ秒かかるとき(window.SLOW = 30)
effect 記録 30 フレーム / ずれた位置(top=192)のフレーム: 3 / 正しい位置(top=134)になった時刻: 67ms
layout 記録 30 フレーム / ずれた位置(top=192)のフレーム: 0 / 正しい位置(top=134)になった時刻: 0ms
3回実行したところ、useEffect版でずれた位置のフレームは3、5、3(正しい位置になるまで67〜117ミリ秒)、useLayoutEffect版は3回とも0でした。
| 条件 | useEffect版 | useLayoutEffect版 |
|---|---|---|
| レンダーが軽い | ずれたフレーム 0 | ずれたフレーム 0 |
| レンダーに30ミリ秒かかる | ずれたフレーム 3〜5(67〜117ミリ秒) | ずれたフレーム 0 |
この結果から、次のことがわかります。
- useLayoutEffect版は、どの条件でもずれた位置が一度も描かれません。 エフェクトの中の
setHeightによる再レンダーまで含めて、描画の前に終わるためです。 - useEffect版は、処理が軽いうちは次の描画に間に合うこともあります。 今回の軽い条件では0フレームでした。
- レンダーが重くなると、useEffect版ではずれた位置が数フレーム表示されます。 「開発中の手元では気付かず、画面が複雑になってからちらつきが見える」という経緯になりやすいのはこのためです。
React公式リファレンスのuseEffectのページにも、エフェクトがクリックなどの操作によるものでない場合、Reactはふつう先にブラウザに画面を描かせてからエフェクトを実行すること、ツールチップの位置決めのように見た目に関わる処理で遅れが目に付く(ちらつく)場合はuseLayoutEffectに置き換えることが書かれています。
直し方:フックの名前を1か所変える
ちらつくコードと直したコードの違いは1行です。
// 位置がずれて見えることがある書き方
useEffect(() => {
setHeight(ref.current.getBoundingClientRect().height);
}, []);
// 描画の前に測って置き直す書き方
useLayoutEffect(() => {
setHeight(ref.current.getBoundingClientRect().height);
}, []);
依存配列やクリーンアップの書き方はそのままで、フックの名前だけを変えます。上の実行結果は、この1行の違いを useHook というpropsで切り替えて確かめたものです。
useLayoutEffectを使う場面・useEffectのままでよい場面
| 場面 | 選ぶフック | 理由 |
|---|---|---|
| ツールチップ・ポップオーバーの位置を、要素のサイズから決める | useLayoutEffect | 仮の位置が描かれるのを避けたい |
| 追加されたメッセージに合わせてスクロール位置を調整する | useLayoutEffect | 調整前の位置が一瞬見えるのを避けたい |
| 文字数に応じて入力欄の高さを変える | useLayoutEffect | 高さが変わる前の表示を避けたい |
| APIからデータを取得する | useEffect | 描画を待たせる理由がない |
| イベントの購読、タイマー、WebSocketの接続 | useEffect | 見た目に直接関わらない |
document.title の変更、アクセスログの送信 |
useEffect | 描画のあとで十分 |
localStorage から値を読んで表示を変える |
まずuseEffect。切り替わりが目に付くならuseLayoutEffect | 変える前の表示が見えるかどうかで決める |
useLayoutEffectの中の処理と、そこからのstate更新による再レンダーは、ブラウザの描画を待たせます。 公式リファレンスには、使いすぎるとアプリが遅くなるので、できるだけuseEffectを選ぶようにと書かれています。「測って、見た目を変える」処理に絞って使うのが目安です。
useInsertionEffectの位置づけ
useInsertionEffectは、useLayoutEffectよりもさらに前に実行されるフックです。公式リファレンスには「CSS-in-JSライブラリの作者向け」と明記されており、アプリのコードで直接使う機会は多くありません。役割は、ほかのエフェクトがレイアウトを読む前に、styleタグを差し込んでおくことです。
次のコードで、3つのフックそれぞれの時点で「refが付いているか」「差し込んだスタイルが効いているか」を調べます。
// b3.jsx(React 19.3.0)
import { useEffect, useLayoutEffect, useInsertionEffect, useRef } from 'react';
import { createRoot } from 'react-dom/client';
function Badge() {
const ref = useRef(null);
const info = (name) => {
const el = document.getElementById('badge');
console.log(
name.padEnd(18),
'| ref.current:', ref.current ? ref.current.tagName : null,
'| 文字色:', el ? getComputedStyle(el).color : '-'
);
};
useInsertionEffect(() => {
info('useInsertionEffect');
const style = document.createElement('style');
style.textContent = '.badge { color: rgb(200, 0, 0); }';
document.head.appendChild(style);
return () => style.remove();
}, []);
useLayoutEffect(() => info('useLayoutEffect'), []);
useEffect(() => info('useEffect'), []);
return <span id="badge" className="badge" ref={ref}>NEW</span>;
}
createRoot(document.getElementById('root')).render(<Badge />);
useInsertionEffect | ref.current: null | 文字色: rgb(0, 0, 0)
useLayoutEffect | ref.current: SPAN | 文字色: rgb(200, 0, 0)
useEffect | ref.current: SPAN | 文字色: rgb(200, 0, 0)
- useInsertionEffectの時点では、refはまだ付いていません(
null)。 - useInsertionEffectで差し込んだスタイルは、useLayoutEffectの時点ですでに効いています(文字色が
rgb(200, 0, 0))。useLayoutEffectでサイズを測る処理は、スタイルが当たった状態の値を読めます。
公式リファレンスの注意事項には、useInsertionEffectの中ではstateを更新できないこと、refがまだ付いていないこと、DOMが更新済みかどうかは決まっていないのでDOMの状態に頼らないことが挙げられています。アプリのコードでは、スタイルはCSSファイルやライブラリに任せ、計測はuseLayoutEffect、それ以外はuseEffectと分けておけば十分です。
サーバーレンダリングでの動作
Next.jsなどでサーバー側でHTMLを作る場合、3つのフックはどれもサーバーでは実行されません。renderToString で確かめます。
// s1.jsx
import { useEffect, useLayoutEffect, useInsertionEffect, useState, version } from 'react';
import { renderToString } from 'react-dom/server';
function Box() {
const [width, setWidth] = useState(0);
console.log('render(サーバー)');
useInsertionEffect(() => { console.log('useInsertionEffect'); }, []);
useLayoutEffect(() => {
console.log('useLayoutEffect');
setWidth(window.innerWidth);
}, []);
useEffect(() => { console.log('useEffect'); }, []);
return <p>width: {width}</p>;
}
console.log('React', version);
const errs = [];
const orig = console.error;
console.error = (fmt, ...args) => {
let i = 0;
errs.push(String(fmt).replace(/%s/g, () => args[i++]).split('\n')[0]);
};
const html = renderToString(<Box />);
console.error = orig;
console.log('HTML:', html);
console.log('console.error の件数:', errs.length);
errs.forEach((e) => console.log(' ' + e));
React 19.3.0での結果:
React 19.3.0
render(サーバー)
HTML: <p>width: <!-- -->0</p>
console.error の件数: 0
React 18.3.1での結果:
React 18.3.1
render(サーバー)
HTML: <p>width: <!-- -->0</p>
console.error の件数: 1
Warning: useLayoutEffect does nothing on the server, because its effect cannot be encoded into the server renderer's output format. This will lead to a mismatch between the initial, non-hydrated UI and the intended UI. To avoid this, useLayoutEffect should only be used in components that render exclusively on the client. See https://reactjs.org/link/uselayouteffect-ssr for common fixes.
| 確かめたこと | React 18.3.1 | React 19.3.0 |
|---|---|---|
| サーバーでレンダーは実行されるか | される | される |
| 3つのエフェクトは実行されるか | されない | されない |
| 出力されるHTML | 初期値(width: 0)のまま |
初期値(width: 0)のまま |
| useLayoutEffectに対する警告 | 出る(useLayoutEffect does nothing on the server) | 出ない |
どちらの版でも、サーバーが返すHTMLは「エフェクトが動く前の初期状態」です。 19では警告が出なくなりましたが、動作は同じです。useLayoutEffectで測った結果が反映されるのは、ブラウザでJavaScriptが読み込まれ、ハイドレーション(サーバーのHTMLにReactをつなぐ処理)が済んだあとになります。
レンダー中にwindowを読むと止まる
サーバーには window も document もありません。レンダー中に読むとエラーになります。
// s2.jsx
import { useLayoutEffect, useState } from 'react';
import { renderToString } from 'react-dom/server';
// A:レンダー中に window を読む
function WidthA() {
const [width] = useState(window.innerWidth);
return <p>width: {width}</p>;
}
// B:useLayoutEffect の中で読む(サーバーでは実行されない)
function WidthB() {
const [width, setWidth] = useState(null);
useLayoutEffect(() => { setWidth(window.innerWidth); }, []);
return <p>width: {width ?? '計測中'}</p>;
}
for (const [name, C] of [['A', WidthA], ['B', WidthB]]) {
try { console.log(name, '->', renderToString(<C />)); }
catch (e) { console.log(name, '->', e.name + ': ' + e.message); }
}
A -> ReferenceError: window is not defined
B -> <p>width: <!-- -->計測中</p>
ブラウザにしかないものは、エフェクトの中で読みます。 サーバーが返すHTMLには、測る前の表示(ここでは「計測中」)を入れておきます。
マウント後に描画する書き方
公式リファレンスのuseLayoutEffectのページには、サーバーレンダリングでの対処として、次の選択肢が挙げられています。
| 選択肢 | 内容 |
|---|---|
| useEffectに置き換える | 最初の表示を待たせなくてよい場合 |
| クライアント専用として印を付ける | 最も近いSuspenseのフォールバックがサーバー側で表示される |
| ハイドレーション後に描画する | isMounted のような真偽値のstateを持ち、useEffectで true にしてから本来の内容を描く |
| useSyncExternalStoreを使う | 外部のデータストアと同期していて、レイアウトの計測が目的ではない場合 |
このうち「ハイドレーション後に描画する」方法を動かします。サーバーと最初のクライアント描画では仮の内容を出し、マウント後に計測つきのコンポーネントへ差し替えます。
// happ.jsx
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
// マウント後に true になるフラグ(サーバーと最初のクライアント描画では false)
export function useIsClient() {
const [isClient, setIsClient] = useState(false);
useEffect(() => { setIsClient(true); }, []);
return isClient;
}
// 自分の幅を測って表示する(クライアント専用の処理を含む)
function Measured() {
const ref = useRef(null);
const [width, setWidth] = useState(null);
useLayoutEffect(() => {
console.log('useLayoutEffect: 幅を計測');
setWidth(Math.round(ref.current.getBoundingClientRect().width));
}, []);
return <p id="m" ref={ref} style={{ width: 240 }}>幅: {width ?? '計測中'}</p>;
}
export function App() {
const isClient = useIsClient();
return (
<main>
<h1>記事</h1>
{isClient ? <Measured /> : <p id="m" style={{ width: 240 }}>幅: 計測中</p>}
</main>
);
}
// サーバー側(Node.js)
import { renderToString } from 'react-dom/server';
import { App } from './happ.jsx';
console.log('サーバーのHTML:', renderToString(<App />));
// ブラウザ側
import { hydrateRoot } from 'react-dom/client';
import { App } from './happ.jsx';
const m = () => document.getElementById('m').textContent;
console.log('ハイドレーション前の表示:', m());
hydrateRoot(document.getElementById('root'), <App />, {
onRecoverableError: (e) => console.log('onRecoverableError:', e.message),
});
setTimeout(() => console.log('ハイドレーション後の表示:', m()), 300);
Node.jsで作ったHTMLをページに置き、Chromiumでハイドレーションした結果です。
サーバーのHTML: <main><h1>記事</h1><p id="m" style="width:240px">幅: 計測中</p></main>
ハイドレーション前の表示: 幅: 計測中
useLayoutEffect: 幅を計測
ハイドレーション後の表示: 幅: 240
console.error の件数: 0
サーバーのHTMLとクライアントの最初の描画が同じ内容なので、ハイドレーションの不一致のエラーは0件です。 そのあと isClient が true になり、計測つきのコンポーネントに差し替わって幅240が表示されました。仮の内容は、差し替え後と同じ大きさにしておくと、画面のがたつきを抑えられます。
つまずきやすい点と直し方
| 症状 | 原因 | 直し方 |
|---|---|---|
| 位置やサイズが一瞬ずれて見える | 計測と置き直しをuseEffectで行っている | useLayoutEffectに変える |
| useLayoutEffectにしたら画面の反応が重い | 描画を待たせる処理が長い | 計測だけを残し、データ取得などはuseEffectへ移す |
サーバーで window is not defined |
レンダー中に window や document を読んでいる |
エフェクトの中で読む |
React 18で useLayoutEffect does nothing on the server |
サーバーで描画されるコンポーネントにuseLayoutEffectがある | マウント後に描画する、またはuseEffectに置き換える |
useInsertionEffectの中で ref.current が null |
refが付く前に実行される | DOMを読む処理はuseLayoutEffectへ |
| 開発中だけエフェクトが2回動く | Strict Modeが、開発時に「実行 → クリーンアップ → 実行」を1回余分に行う | クリーンアップで元に戻す処理を書く |
最後の行は、公式リファレンスのuseLayoutEffect・useEffectの両方の注意事項に書かれている動作です。useLayoutEffectでも同じように起きるので、イベントの登録やオブザーバーの開始を書いたら、対になる解除をクリーンアップに書いておきます。
よくある質問
Q. useEffectを全部useLayoutEffectにしておけば、ちらつきの心配がなくなりますか? A. ちらつきは出なくなりますが、描画を待たせる処理が増えて、画面の反応が遅くなります。公式リファレンスでも、できるだけuseEffectを選ぶように書かれています。useLayoutEffectは「DOMを測って、その結果で見た目を変える」処理に絞るのが目安です。
Q. useEffectでも、ちらつかないことがあるのはなぜですか? A. 今回の計測でも、レンダーが軽い条件ではuseEffect版のずれたフレームは0でした。エフェクトと再レンダーが次の描画までに終われば、仮の状態は画面に出ません。ただし、レンダーに30ミリ秒かかる条件では3〜5フレームの間ずれた位置が出ました。端末の速さや画面の複雑さで結果が変わるので、計測して見た目を変える処理は最初からuseLayoutEffectにしておくと安定します。
Q. useLayoutEffectの中で setState を呼んでもよいですか?
A. 呼べます。その更新による再レンダーは、ブラウザの描画の前に済みます。ツールチップの例で、ずれた位置が一度も描かれなかったのはこの動作によるものです。更新のたびに描画が待たされるので、必要な計測に絞って使います。
Q. Next.jsなどサーバーで描画する構成でも、useLayoutEffectを使えますか?
A. 使えます。サーバーではエフェクトが実行されず、HTMLには初期状態が入ります。計測結果が反映されるのはハイドレーションのあとなので、初期状態の表示を用意しておくか、今回の useIsClient のようにマウント後に描画する形にします。React 19.3.0では、renderToString でuseLayoutEffectを含むコンポーネントを描いても警告は出ませんでした。
まとめ
- useLayoutEffectは描画の前、useEffectは描画のあと。 書き方は同じで、違いは実行タイミングだけ。
- 実行順は、レンダー(親→子)→ useInsertionEffect → useLayoutEffect → 描画 → useEffect(エフェクトは子→親)。 更新時はクリーンアップが先、アンマウント時のクリーンアップは親→子。
- 計測して見た目を変える処理をuseEffectに書くと、レンダーが重いときに仮の状態が表示される。 今回は30ミリ秒のレンダーで3〜5フレームずれ、useLayoutEffectでは0だった。
- useLayoutEffectは描画を待たせるので、計測と置き直しに絞る。 データ取得や購読はuseEffect。
- useInsertionEffectはCSS-in-JSライブラリ向け。 refが付く前に実行され、差し込んだスタイルはuseLayoutEffectの時点で効いている。
- サーバーでは3つとも実行されない。
windowはエフェクトの中で読み、必要ならマウント後に描画する。React 18.3.1では警告が出て、19.3.0では出なかった。
参考・出典
- React 公式リファレンス「useLayoutEffect」:https://react.dev/reference/react/useLayoutEffect
- React 公式リファレンス「useInsertionEffect」:https://react.dev/reference/react/useInsertionEffect
- React 公式リファレンス「useEffect」:https://react.dev/reference/react/useEffect
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「React useLayoutEffectとuseEffectの違いと実行順」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。