落とし穴

useLayoutEffectはパフォーマンスを低下させる可能性があります。可能な限りuseEffectを使用してください。

useLayoutEffectは、ブラウザが画面を再描画する前に実行されるuseEffectのバージョンです。

useLayoutEffect(setup, dependencies?)

リファレンス

useLayoutEffect(setup, dependencies?)

ブラウザが画面を再描画する前にレイアウト測定を実行するには、useLayoutEffectを呼び出します。

import { useState, useRef, useLayoutEffect } from 'react';

function Tooltip() {
const ref = useRef(null);
const [tooltipHeight, setTooltipHeight] = useState(0);

useLayoutEffect(() => {
const { height } = ref.current.getBoundingClientRect();
setTooltipHeight(height);
}, []);
// ...

以下の例を参照してください。

パラメータ

  • setup: エフェクトのロジックを含む関数。setup関数は、クリーンアップ関数もオプションで返すことができます。コンポーネントがDOMに追加される前に、Reactはsetup関数を呼び出します。依存関係が変更された再レンダリングの後、Reactは最初に古い値でクリーンアップ関数(指定されている場合)を実行し、次に新しい値でsetup関数を呼び出します。コンポーネントがDOMから削除される前に、Reactはクリーンアップ関数を呼び出します。

  • オプション dependencies: setupコード内で参照されるすべてのリアクティブ値のリスト。リアクティブ値には、props、state、およびコンポーネント本体内で直接宣言されたすべての変数と関数が含まれます。リンターがReact用に設定されている場合、すべてのリアクティブ値が依存関係として正しく指定されていることを確認します。依存関係のリストには、一定数のアイテムが含まれており、[dep1, dep2, dep3]のようにインラインで記述する必要があります。Reactは、Object.is比較を使用して、各依存関係とその以前の値を比較します。この引数を省略すると、エフェクトはコンポーネントの再レンダリングのたびに再実行されます。

戻り値

useLayoutEffectundefinedを返します。

注意点

  • useLayoutEffect

  • 厳格モードが有効な場合、React は最初の実際のセットアップの前に、開発時のみの追加のセットアップとクリーンアップのサイクルを1回実行します。これは、クリーンアップロジックがセットアップロジックを「反映」し、セットアップが実行している処理を停止または元に戻すことを確認するためのストレステストです。これが問題を引き起こす場合は、クリーンアップ関数を実装してください。

  • 依存関係の一部がコンポーネント内で定義されたオブジェクトまたは関数である場合、それらが必要以上にEffectを再実行する可能性があります。これを修正するには、不要なオブジェクト関数の依存関係を削除します。また、状態の更新を抽出したり、非リアクティブなロジックをEffectの外に抽出することもできます。

  • Effectはクライアントでのみ実行されます。サーバーサイドレンダリング中は実行されません。

  • useLayoutEffect内にあるコードと、そこからスケジュールされたすべての状態更新は、ブラウザによる画面の再描画をブロックします。過剰に使用すると、アプリケーションが遅くなります。可能な限り、useEffectを使用することをお勧めします。

  • useLayoutEffect内で状態更新をトリガーすると、ReactはuseEffectを含む残りのすべてのEffectをすぐに実行します。


使用方法

ブラウザが画面を再描画する前にレイアウトを測定する

ほとんどのコンポーネントは、レンダリングするものを決定するために、画面上の位置とサイズを知る必要はありません。JSX を返すだけです。その後、ブラウザがレイアウト(位置とサイズ)を計算し、画面を再描画します。

しかし、時にはそれで不十分な場合があります。ホバー時に要素の横に表示されるツールチップを想像してください。十分なスペースがあれば、ツールチップは要素の上に表示されますが、スペースが足りない場合は下に表示されます。ツールチップを正しい最終位置にレンダリングするには、その高さ(つまり、上に収まるかどうか)を知る必要があります。

そのためには、2 パスでレンダリングする必要があります。

  1. ツールチップをどこにでもレンダリングします(間違った位置でも)。
  2. 高さを測定し、ツールチップを配置する場所を決定します。
  3. ツールチップを再び正しい場所にレンダリングします。

これらすべては、ブラウザが画面を再描画する前に実行される必要があります。ユーザーにツールチップが移動している様子を見せたくありません。useLayoutEffectを呼び出して、ブラウザが画面を再描画する前にレイアウト測定を実行します。

function Tooltip() {
const ref = useRef(null);
const [tooltipHeight, setTooltipHeight] = useState(0); // You don't know real height yet

useLayoutEffect(() => {
const { height } = ref.current.getBoundingClientRect();
setTooltipHeight(height); // Re-render now that you know the real height
}, []);

// ...use tooltipHeight in the rendering logic below...
}

これがステップバイステップでどのように機能するかを示します。

  1. Tooltipは、初期のtooltipHeight = 0でレンダリングされます(そのため、ツールチップの位置が間違っている可能性があります)。
  2. React はそれを DOM に配置し、useLayoutEffectのコードを実行します。
  3. あなたのuseLayoutEffectは、ツールチップコンテンツの高さを測定し、即座に再レンダリングをトリガーします。
  4. Tooltipは、実際のtooltipHeightで再びレンダリングされます(そのため、ツールチップは正しく配置されます)。
  5. React はそれを DOM で更新し、ブラウザは最終的にツールチップを表示します。

下のボタンにカーソルを合わせると、ツールチップが収まるかどうかに応じて位置が調整される様子を確認できます。

import { useRef, useLayoutEffect, useState } from 'react';
import { createPortal } from 'react-dom';
import TooltipContainer from './TooltipContainer.js';

export default function Tooltip({ children, targetRect }) {
  const ref = useRef(null);
  const [tooltipHeight, setTooltipHeight] = useState(0);

  useLayoutEffect(() => {
    const { height } = ref.current.getBoundingClientRect();
    setTooltipHeight(height);
    console.log('Measured tooltip height: ' + height);
  }, []);

  let tooltipX = 0;
  let tooltipY = 0;
  if (targetRect !== null) {
    tooltipX = targetRect.left;
    tooltipY = targetRect.top - tooltipHeight;
    if (tooltipY < 0) {
      // It doesn't fit above, so place below.
      tooltipY = targetRect.bottom;
    }
  }

  return createPortal(
    <TooltipContainer x={tooltipX} y={tooltipY} contentRef={ref}>
      {children}
    </TooltipContainer>,
    document.body
  );
}

Tooltipコンポーネントが2パスでレンダリングする必要がある(最初にtooltipHeight0に初期化され、次に実際の測定された高さでレンダリングされる)にもかかわらず、最終的な結果しか表示されないことに注意してください。このため、この例ではuseEffectではなくuseLayoutEffectが必要になります。違いを詳細に見ていきましょう。

useLayoutEffect 対 useEffect

1 2:
useLayoutEffect は、ブラウザが再描画するのをブロックします

React は、useLayoutEffect 内のコードと、その中でスケジュールされた状態の更新が、ブラウザが画面を再描画する前に処理されることを保証します。これにより、ツールチップをレンダリングし、サイズを測定し、ツールチップを再度レンダリングしても、ユーザーは最初の余分なレンダリングに気付くことはありません。言い換えると、useLayoutEffect はブラウザの描画をブロックします。

import { useRef, useLayoutEffect, useState } from 'react';
import { createPortal } from 'react-dom';
import TooltipContainer from './TooltipContainer.js';

export default function Tooltip({ children, targetRect }) {
  const ref = useRef(null);
  const [tooltipHeight, setTooltipHeight] = useState(0);

  useLayoutEffect(() => {
    const { height } = ref.current.getBoundingClientRect();
    setTooltipHeight(height);
  }, []);

  let tooltipX = 0;
  let tooltipY = 0;
  if (targetRect !== null) {
    tooltipX = targetRect.left;
    tooltipY = targetRect.top - tooltipHeight;
    if (tooltipY < 0) {
      // It doesn't fit above, so place below.
      tooltipY = targetRect.bottom;
    }
  }

  return createPortal(
    <TooltipContainer x={tooltipX} y={tooltipY} contentRef={ref}>
      {children}
    </TooltipContainer>,
    document.body
  );
}

注意

2 回のパスでのレンダリングとブラウザのブロックは、パフォーマンスを低下させます。可能な限りこれを避けてください。


トラブルシューティング

エラーが発生しています:「useLayoutEffect はサーバーでは何も実行しません」

useLayoutEffect の目的は、コンポーネントがレイアウト情報を使用してレンダリングできるようにすることです:

  1. 初期コンテンツをレンダリングします。
  2. ブラウザが画面を再描画する前にレイアウトを測定します。
  3. 読み取ったレイアウト情報を使用して、最終的なコンテンツをレンダリングします。

あなたまたはあなたのフレームワークがサーバーサイドレンダリングを使用する場合、React アプリは初期レンダリングのためにサーバー上で HTML にレンダリングされます。これにより、JavaScript コードが読み込まれる前に初期 HTML を表示できます。

問題は、サーバーにはレイアウト情報がないことです。

前の例では、Tooltip コンポーネントのuseLayoutEffect呼び出しにより、コンテンツの高さに応じて(コンテンツの上または下に)正しく配置されます。初期サーバー HTML の一部としてTooltipをレンダリングしようとすると、これは決定できません。サーバー上では、まだレイアウトがありません!そのため、サーバー上でレンダリングしたとしても、JavaScript が読み込まれて実行された後、クライアント上でその位置が「ジャンプ」します。

通常、レイアウト情報に依存するコンポーネントは、サーバー上でレンダリングする必要はありません。たとえば、初期レンダリング中にTooltipを表示することはおそらく意味がありません。クライアントのインタラクションによってトリガーされます。

ただし、この問題が発生している場合は、いくつかの選択肢があります。

  • useLayoutEffectuseEffectに置き換えます。これにより、React は(元の HTML が Effect が実行される前に表示されるため)、ペイントをブロックせずに初期レンダリング結果を表示しても問題ないことがわかります。

  • あるいは、コンポーネントをクライアント専用としてマークします。これにより、React はサーバーサイドレンダリング中にそのコンテンツを最も近い<Suspense>境界まで、ローディングフォールバック(たとえば、スピナーまたはグリマー)に置き換えます。

  • あるいは、ハイドレーション後にのみuseLayoutEffectを使用してコンポーネントをレンダリングすることもできます。`false`に初期化され、useEffect呼び出し内で`true`に設定されるブール値のisMounted状態を保持します。レンダリングロジックは、その後return isMounted ? <RealContent /> : <FallbackContent />のようになります。サーバー上とハイドレーション中は、ユーザーはFallbackContentが表示され、これはuseLayoutEffectを呼び出すべきではありません。その後、React はそれをuseLayoutEffect呼び出しを含めることができるクライアント上でのみ実行されるRealContentに置き換えます。

  • 外部データストアとコンポーネントを同期し、レイアウトの測定以外の理由でuseLayoutEffectに依存する場合は、useSyncExternalStoreを代わりに検討してください。これはサーバーサイドレンダリングをサポートしています。