メインコンテンツへスキップ

useLayoutEffect 完全入門【React】描画前に実行するエフェクト

ReactのuseLayoutEffectとは何か、useEffectとの違いや使いどころを初心者向けに解説。ツールチップの位置計算など、レイアウト測定が必要な場面での使い方を紹介します。

#react#hooks#uselayouteffect#javascript#frontend

useEffect を使っていると、まれに「画面がちらつく」「要素の位置が一瞬ずれる」という問題に遭遇することがあります。そんなときの解決策として用意されているのが useLayoutEffect です。

ほとんどのケースでは useEffect で十分ですが、DOM の測定結果をもとに再描画が必要な場面では useLayoutEffect が適しています。

この記事でわかること:

  • useLayoutEffect が必要な場面
  • useEffect との実行タイミングの違い
  • ツールチップなどのレイアウト計算での使い方
  • よくある落とし穴とパフォーマンスへの影響
  • サーバーサイドレンダリングでの注意点
  • useEffect から useLayoutEffect への切り替えタイミング

useLayoutEffect とは?

useLayoutEffect は、ブラウザが画面を再描画する前に実行されるエフェクトフックです。

useLayoutEffect(setup関数, 依存配列);

useEffect と API はまったく同じです。違うのは実行タイミングだけです。

フック実行タイミング
useEffectブラウザが画面を描画した
useLayoutEffectブラウザが画面を描画する

useEffect との違いを図で理解する

【useEffect の流れ】
① React がDOM を更新する
② ブラウザが画面を描画する  ← ユーザーに見える
③ useEffect が実行される

【useLayoutEffect の流れ】
① React が DOM を更新する
② useLayoutEffect が実行される  ← 描画前に処理!
③ ブラウザが画面を描画する  ← ユーザーに見える

useLayoutEffect は描画をブロックして先に処理するため、「一瞬ちらつく」ような問題が起きないのが特徴です。

なぜちらつきが起きるのか?

useEffect を使って DOM のサイズを測定し、状態を更新する場合を考えてみましょう。

  1. 最初の状態でレンダリング(例:ツールチップが上に表示)
  2. ブラウザが上記の状態を描画(ユーザーに一瞬見える)
  3. useEffect が実行され「スペースが足りない」と判定
  4. 状態更新→再レンダリング(ツールチップが下に移動)
  5. ブラウザが再描画

この2→3→4のステップで「一瞬ずれた位置が見える」→「正しい位置に移動する」というちらつきが発生します。

useLayoutEffect を使うと、ステップ2(ブラウザの描画)の前に位置調整が完了するため、ユーザーには最初から正しい位置が表示されます。

実用例1:ツールチップの位置を動的に計算する

ツールチップは表示するためのスペースが足りないとき、位置を上下に切り替える必要があります。これを useEffect でやると位置が一瞬ずれることがありますが、useLayoutEffect を使えばスムーズに表示できます。

import { useRef, useState, useLayoutEffect } from "react";
 
function Tooltip({ children, text }) {
  const tooltipRef = useRef(null);
  const [position, setPosition] = useState("above"); // 初期位置は上
 
  useLayoutEffect(() => {
    const tooltip = tooltipRef.current;
    const rect = tooltip.getBoundingClientRect();
 
    // ツールチップが画面上部にはみ出す場合は下に表示
    if (rect.top < 0) {
      setPosition("below");
    } else {
      setPosition("above");
    }
  });
 
  return (
    <div>
      <div
        ref={tooltipRef}
        style={{
          position: "absolute",
          top: position === "above" ? "-40px" : "100%",
        }}
      >
        {text}
      </div>
      {children}
    </div>
  );
}

このコードでは:

  1. React が DOM を更新する
  2. useLayoutEffect が実行され、ツールチップの位置を測定・調整する
  3. 調整済みの状態でブラウザが描画する

ユーザーには「最初から正しい位置に表示されたツールチップ」として見えます。

実用例2:スクロール位置の復元

ページ遷移後にスクロール位置を特定の位置に戻す場合も useLayoutEffect が適しています。useEffect で行うと、一瞬ページトップが見えてからスクロールするため不自然です。

import { useLayoutEffect } from "react";
import { useLocation } from "react-router-dom";
 
function ScrollRestorer() {
  const { pathname } = useLocation();
 
  // ページ遷移時にスクロール位置をトップに戻す(描画前に実行)
  useLayoutEffect(() => {
    window.scrollTo(0, 0);
  }, [pathname]);
 
  return null;
}

実用例3:要素の高さに応じたレイアウト調整

子要素の高さを動的に測定してレイアウトを調整する場面でも useLayoutEffect が役立ちます。

import { useRef, useState, useLayoutEffect } from "react";
 
function AdaptivePanel({ children }) {
  const contentRef = useRef(null);
  const [headerHeight, setHeaderHeight] = useState(0);
 
  useLayoutEffect(() => {
    if (contentRef.current) {
      // DOM のサイズを測定してから状態を更新
      setHeaderHeight(contentRef.current.offsetHeight);
    }
  }, [children]); // children が変わるたびに再計算
 
  return (
    <div>
      <div ref={contentRef} style={{ /* ヘッダースタイル */ }}>
        {children}
      </div>
      <div style={{ marginTop: headerHeight + "px" }}>
        {/* ヘッダーの高さに合わせてメインコンテンツを配置 */}
      </div>
    </div>
  );
}

パフォーマンスへの影響

useLayoutEffect は描画をブロックします。処理が重いとその分だけ画面の表示が遅れてしまいます。

// ❌ NG:重い処理を useLayoutEffect で書かない
useLayoutEffect(() => {
  // 1万件のデータを処理する、など重い処理
  processHeavyData();
}, [data]);
 
// ✅ OK:軽いレイアウト測定に限定する
useLayoutEffect(() => {
  const height = ref.current.offsetHeight;
  setHeight(height);
}, []);

原則として useEffect を使い、ちらつき問題が起きた場合にのみ useLayoutEffect に切り替えるのがベストプラクティスです。

よくある落とし穴

落とし穴1:無限ループ

状態の更新が依存配列に含まれている場合、無限ループが起きることがあります。

// ❌ 無限ループの例
const [height, setHeight] = useState(0);
 
useLayoutEffect(() => {
  setHeight(ref.current.offsetHeight);
  // height を依存配列に入れると、setHeight → 再レンダリング → 再実行 のループ
}, [height]); // ← NG
 
// ✅ 依存配列を適切に設定する
useLayoutEffect(() => {
  setHeight(ref.current.offsetHeight);
}, []); // 初回のみ実行

落とし穴2:ref が null になる

useLayoutEffect はレンダリング直後に実行されますが、ref.currentnull の場合があります。必ず null チェックを行いましょう。

useLayoutEffect(() => {
  if (!ref.current) return; // null チェック必須
  const rect = ref.current.getBoundingClientRect();
  // ...
}, []);

useEffect vs useLayoutEffect の選び方

useEffect を選ぶ場面(9割以上のケース):
- データのフェッチ
- イベントリスナーの登録・解除
- タイマーの設定
- 外部ライブラリの初期化
- ログの送信

useLayoutEffect を選ぶ場面(特殊なケース):
- DOM のサイズ・位置を測定して状態を更新する
- ツールチップ・ポップオーバーの位置計算
- スクロール位置の制御(ちらつきを防ぐ)
- アニメーションの開始位置の設定

迷ったら useEffect を使い、「ちらつく」「一瞬ずれる」問題が起きたときだけ useLayoutEffect を試しましょう。

サーバーサイドレンダリングでの注意

useLayoutEffect はサーバー(Node.js)では実行されません。そのため、Next.js などの SSR 環境で使うと以下の警告が表示されることがあります:

Warning: useLayoutEffect does nothing on the server...

SSR 環境での対処法はいくつかあります。

方法1:useEffect で代用できないか検討する(推奨)

多くの場合、SSR でちらつきが問題になることはありません。まず useEffect で試してみましょう。

方法2:クライアント判定でガードする

import { useEffect, useLayoutEffect } from "react";
 
// SSR 対応の useLayoutEffect
const useIsomorphicLayoutEffect =
  typeof window !== "undefined" ? useLayoutEffect : useEffect;

このパターンはよく使われる慣例で、サーバーでは useEffect(何もしない)、クライアントでは useLayoutEffect として動作します。

まとめ

  • useLayoutEffectブラウザ描画前に実行されるエフェクト
  • useEffect と API は同じで、実行タイミングだけが違う
  • ツールチップの位置計算など、DOM 測定→再描画が必要な場面で使う
  • 描画をブロックするため、重い処理には使わない
  • SSR 環境では実行されないことに注意
  • 基本は useEffect を使い、ちらつきが起きたら useLayoutEffect を検討

PR

useLayoutEffect 公式ドキュメントで詳細を確認しよう

useLayoutEffectのインタラクティブなサンプルや、useEffectとの使い分けの詳細は公式ドキュメントで確認できます。

useLayoutEffect 公式ドキュメントを見る

PR

関連記事