Reactの「同期」と「非同期」の境界線:useEffectとuseLayoutEffectを使いこなす極意
Reactで開発をしていると、避けては通れないのが「画面がチカッと瞬きする(フリッカー)」現象だ。CSSで頑張っても直らない、あの忌々しいチラつき。もし君がそれを「Reactあるある」で片付けているなら、今日でその認識を捨ててほしい。
今回は、Reactにおける副作用の制御、特に `useEffect` と `useLayoutEffect` の決定的な違いについて、ブラウザの裏側で何が起きているのかという「泥臭い現場の知識」を共有する。
—
ブラウザの描画パイプラインを知る
まず、ブラウザが画面を表示するまでの流れを整理しよう。
1. JavaScriptの実行: ReactがDOMを更新する。
2. ブラウザによる計算: スタイル計算とレイアウト(Reflow)が行われる。
3. ペイント: 画面が描画される。
ここで重要なのは、`useEffect` は「ペイントの後」に実行されるということだ。Reactは画面を描画し、ユーザーにコンテンツを見せた「後」に処理を走らせる。これこそが、パフォーマンスを維持するためのReactの知恵だ。
一方で、`useLayoutEffect` は 「DOMが更新された直後、かつペイントの前」 に同期的に実行される。
なぜ `useLayoutEffect` が必要なのか?
「ペイントの前に実行される」ということは、ブラウザが画面を表示する前に、ReactがDOMの状態を強制的に書き換えることができることを意味する。
もし `useEffect` でDOMのサイズを取得してスタイルを書き換えると、どうなるか。
1. Reactが一度レンダリングして画面に出す(ここで不完全な状態が見える)。
2. `useEffect` が動く。
3. DOMを操作する。
4. ブラウザが再計算し、画面が更新される。
この「2回の描画」の隙間に、あのチラつきが発生するんだ。これを防ぐために `useLayoutEffect` を使う。
実践:チラつきを封じ込めるコード
例えば、ツールチップやモーダルの位置を、DOMのサイズに基づいて動的に計算するようなケースを想像してほしい。
import React, { useState, useLayoutEffect, useRef } from ‘react’;
const Tooltip = () => {
const [position, setPosition] = useState(0);
const elementRef = useRef(null);
// useEffectだと画面描画後に計算するため、一瞬ズレてから位置が修正される
// useLayoutEffectなら描画前に同期的に計算するため、ユーザーには最初から正しい位置に見える
useLayoutEffect(() => {
if (elementRef.current) {
// DOMの実際の幅や高さを取得して、位置を再計算する
const { offsetWidth } = elementRef.current;
// 例えば、親要素の中央に配置するための補正処理など
const calculatedPosition = window.innerWidth / 2 – offsetWidth / 2;
setPosition(calculatedPosition);
}
}, []); // 初回マウント時のみ実行
return (
私は計算された位置に表示されます!
);
};
現場で使い分けるための鉄則
「とりあえず `useLayoutEffect` を使えばいいじゃん」と思った君、それは危険だ。
`useLayoutEffect` は同期的に実行されるため、その中で重い処理を走らせると、ブラウザの描画そのものをブロックしてしまう。 結果として、アプリ全体が重く、クリックに対する反応が鈍い「モッサリしたUI」が出来上がる。
以下の基準を心に刻んでおいてくれ。
- 基本は `useEffect`: ほとんどの副作用(API通信、イベントリスナーの登録、ログ送信など)はこれで十分だ。パフォーマンスへの影響を最小限に抑えるのがエンジニアの矜持だよ。
- 例外は `useLayoutEffect`: DOMのサイズ、位置、スクロール位置など、「画面が描画される前に確定していなければならない情報」を使って、DOMを書き換える時だけこれを使う。
まとめ
Reactは「UIと状態の同期」を簡潔に書ける素晴らしいライブラリだが、その裏側にあるブラウザのレンダリングの仕組みを無視すると、必ずどこかで綻びが出る。
「なぜこのフックを使うのか?」という問いに対して、「ブラウザのペイント処理をブロックせず、しかしチラつきを回避するために、このタイミングで同期的に実行する必要があるからだ」 と、自信を持って答えられるようになってほしい。
現場でバグに悩んだとき、この「同期と非同期の境界線」を思い出せば、必ず解決の糸口が見えるはずだ。頑張ろうぜ。

コメント