定義
子コンポーネントで「親が渡した DOM 要素」に属性を付けたりサイズを測ろうとするのに、確かに画面には描画されているのにその DOM が null で取れることがある。この概念は、そのタイミング問題を理解して安全に避けようとするときに使う。
React commit(仮想 DOM の計算が終わった後に実際のブラウザ DOM へ変更を反映する段階)は一般に子 DOM ノードから親方向へ(child-first、ツリーの下側のノードから上へ付ける)接続する。したがって子コンポーネントの useLayoutEffect(DOM 変更直後・画面に描く前に同期で実行される effect)が実行される時点では、親が渡した ref.current(親 DOM 要素を指す参照の箱)がまだ null でありうる。祖先 DOM 要素に属性を書いたり測定したりするには、useEffect(画面に描いた後に実行される passive effect)に遅延するか、コールバック ref・自分の DOM ref に対象を変える必要がある。
文脈で理解する
子コンポーネントが「文書が空か」を知って親が渡した container refに data-empty のような属性を付けようとするとき、store には値があるのに ref.current だけ null の場合がある。React は DOM を下から上へ付けるため、子 useLayoutEffect が回るとき親 ref はまだ接続前でありうる。バグのように見えるがcommit 順序の問題だ。
なぜ必要か
子が useLayoutEffect で containerRef.current.setAttribute('data-empty', …) のように親 refへ書き込むと、commit 順序のせいでsilent no-op になる。データ・store スナップショットはあるのに el === null のデバッグログが残る。「layout effect の方が速いから」祖先 ref に書くのはよくある落とし穴だ。
動作原理
React の更新はおおよそ次の順序だ。
- Trigger — state/props 変更
- Render — Virtual DOM 計算
- Commit — 実際の DOM 変更(通常 child-first)
- Layout effects —
useLayoutEffect(paint 前、同期) - Paint
- Passive effects —
useEffect(commit 完了後)
| 段階 | 祖先 ref.current | 適した用途 |
|---|---|---|
子 useLayoutEffect | しばしば null | 自分の DOM 測定・同期調整 |
子 useEffect | 通常は接続済み | 祖先 ref への書き込み・非緊急の副作用 |
| コールバック ref(親) | 親が直接制御 | 親が子の信号を受けて DOM ペイント |
属性が 1 フレーム遅れて付くことがある。視覚的に critical でないフラグ(空文書表示など)には許容される場合が多い。
実務適用
- 祖先/コンテナ ref への書き込みは子
useEffectで行う。 - 同期測定がどうしても必要なら、ref の対象を自分の DOMへ移すか、親がコールバック refで直接ペイントする。
- コンテナの再レンダーを避けるには
return nullリーフ + external store 購読でフラグだけ分離する。 - Strict Mode の double-mount 時は effect cleanup で属性を消す。
トレードオフ
useEffectは paint 以降なので属性・測定が 1 ティック遅い — ちらつきが見えるならコールバック ref か親側ロジックを検討する。useLayoutEffectはメインスレッドをブロックする — 祖先 ref アクセス用に使ってはいけない。- Portal の子は別の commit サブツリー — 祖先 ref のルールが変わりうる。
使ってはいけない場合
- 自分の DOM のサイズをpaint 前に必ず読む必要がある場合 — 対象 ref を自分のノードへ移してから
useLayoutEffectを使う。 - 親 ref がすでに callback ref で接続された直後だという保証が API 契約にある特殊なラッパー(まれ)。
よくある間違い
- 「速く描こうと」祖先 ref に
useLayoutEffectを使う。 containerRef.currentが null なのに early return なしでロジックだけスキップし、原因把握を先送りする。- 子 layout effect で祖先の scrollHeight を測定 — 誤った 0 または例外。
- Context で変わる doc を Provider value に入れてコンテナが毎 commit ごとに再レンダー — ref タイミングとは別の性能問題。
関連概念
- critical-rendering-path — DOM・スタイル・レイアウト・ペイント順序
- react-context-render-granularity — コンテナ vs リーフ購読者の分離
- fixed-position-floating-ui-scroll — ref ベース座標 UI の別のタイミング問題