1. ホーム
  2. React

【React】useLayoutEffect の使い方|useEffect との違いとちらつきを防ぐ場面

Share

React で副作用を扱うときは useEffect を使うのが基本ですが、まれに「画面に表示される前にDOMを測って調整したい」という場面があります。そこで登場するのが useLayoutEffect です。useEffect とほぼ同じ書き方でありながら、実行されるタイミングだけが異なります。この記事では、useLayoutEffect の基本的な使い方、useEffect との実行タイミングの違い、そして実際にちらつきを防ぎたい場面での使いどころと、選ぶときの指針まで、初心者〜中級者向けに解説します。

useLayoutEffect と useEffect の違い

どちらも「レンダリング後に副作用を実行する」フックですが、実行されるタイミングが違います。React がDOMを更新したあと、ブラウザが実際に画面を描画する前に同期的に実行されるのが useLayoutEffect、描画されたあとに実行されるのが useEffect です。処理の流れを表で比べます。

観点useEffectuseLayoutEffect
実行タイミング画面が描画された後DOM更新後・描画前
実行のされ方非同期(描画をブロックしない)同期(描画をブロックする)
DOM測定の結果反映一瞬ちらつくことがあるちらつかない
基本的な使いどころデータ取得・購読など大半の副作用レイアウト測定と即時の見た目調整

useLayoutEffect は描画前に同期的に走るため、そこでDOMのスタイルを変更しても、ユーザーは変更前の状態を目にしません。一方 useEffect は描画後に走るので、いったん表示されてから調整されると、その差が一瞬のちらつきとして見えることがあります。この違いが唯一にして最大のポイントです。

基本の書き方

書き方は useEffect とまったく同じです。第1引数に副作用の関数、第2引数に依存配列を渡します。関数からクリーンアップ関数を返せる点も共通です。

basic.tsx
import { useLayoutEffect, useRef } from "react";

function Box() {
  const ref = useRef<HTMLDivElement>(null);

  useLayoutEffect(() => {
    if (ref.current) {
      // 描画される前に要素の高さを測れる
      const height = ref.current.getBoundingClientRect().height;
      console.log("高さ:", height);
    }
  }, []); // 依存配列は useEffect と同じルール

  return <div ref={ref}>測定対象</div>;
}

useRef で対象の要素を参照し、useLayoutEffect の中で getBoundingClientRect() を使って高さを測っています。このタイミングではすでにDOMは更新済みなので正しいサイズが取れ、かつまだ描画前なので、ここでスタイルを変えても画面のちらつきになりません。

ちらつきが問題になる例

典型的なのが、要素のサイズや位置を測ってから、その結果に応じて表示位置を決めるケースです。たとえばツールチップを対象要素の真上に出したい場合、ツールチップの高さを測ってから位置を計算する必要があります。この「測って→位置を反映する」流れを useEffect でやると、いったん仮の位置で描画されてから正しい位置に飛ぶため、ちらつきとして見えることがあります。

Tooltip.tsx
import { useLayoutEffect, useRef, useState } from "react";

function Tooltip({ text }: { text: string }) {
  const ref = useRef<HTMLDivElement>(null);
  const [top, setTop] = useState(0);

  useLayoutEffect(() => {
    if (ref.current) {
      // ツールチップ自身の高さぶん上にずらす
      const { height } = ref.current.getBoundingClientRect();
      setTop(-height - 8);
    }
  }, [text]);

  return (
    <div ref={ref} style={{ position: "absolute", top }}>
      {text}
    </div>
  );
}

ツールチップの高さを測り、その値をもとに top を決めています。useLayoutEffect の中で setTop を呼ぶと、React は描画前に再レンダリングを行い、位置が確定した状態で初めて画面に表示されます。そのため、ユーザーには最初から正しい位置のツールチップだけが見え、位置がずれてから直る様子は見えません。同じコードを useEffect にすると、仮位置で一度描画されてから位置が変わるため、環境によっては一瞬のちらつきが出ます。

基本は useEffect、必要なときだけ useLayoutEffect

useLayoutEffect は描画をブロックして同期的に実行されます。つまり、中の処理が重いとその分だけ画面表示が遅れます。便利だからと何でもこれに置き換えると、かえって表示が遅くなることがあります。使い分けの基本方針は次のとおりです。

  • データ取得、イベントの購読、ログ送信など大半の副作用は useEffect を使う
  • DOMのサイズ・位置を測って、その結果を描画前に見た目へ反映したいときだけ useLayoutEffect を使う
  • 迷ったらまず useEffect で書き、ちらつきが気になったときに useLayoutEffect へ切り替える

ほとんどのケースは useEffect で問題ありません。useLayoutEffect は「描画前に測って直す」という限られた目的のための道具だと考えると、使いどころを見誤りにくくなります。

サーバーサイドレンダリングでの警告

サーバーサイドレンダリング(SSR)を行う環境で useLayoutEffect を使うと、コンソールに警告が表示されることがあります。これは、サーバー側にはレイアウトを行うDOMが存在せず、useLayoutEffect が実行されないことに起因します。

対処としては、レイアウト測定が本当に必要かを見直し、不要なら useEffect に変えるのが基本です。どうしても描画前の測定が必要な場合は、その処理をクライアント側でのみ動かすよう、マウント後に実行される仕組みと組み合わせて設計します。SSR 環境では「サーバーとクライアントで見た目が一致しているか」も意識し、初期表示に影響する測定処理はできるだけ避けるのが安全です。

まとめ

useLayoutEffect は、DOMの更新後・ブラウザの描画前に同期的に副作用を実行するフックです。書き方は useEffect と同じで、違うのは実行タイミングだけです。getBoundingClientRect() などで要素のサイズや位置を測り、その結果を描画前に見た目へ反映したい場面では、useEffect で起きがちな一瞬のちらつきを防げます。ただし描画をブロックするため、大半の副作用は useEffect を使い、レイアウト測定と即時反映が必要なときだけ useLayoutEffect を選ぶのが基本です。SSR 環境では警告が出ることも踏まえ、用途を絞って活用してみてください。

参考ページ