useLayoutEffect

陷阱

useLayoutEffect 可能會損害效能。盡可能使用 useEffect

useLayoutEffectuseEffect 的一個版本,它會在瀏覽器重新繪製螢幕之前觸發。

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:包含 Effect 邏輯的函式。您的 setup 函式也可以選擇性地返回一個 *cleanup* 函式。在您的元件被添加到 DOM 之前,React 將會執行您的 setup 函式。在每次依賴項更改後的重新渲染之後,React 將首先使用舊值執行 cleanup 函式(如果您有提供的話),然後使用新值執行您的 setup 函式。在您的元件從 DOM 中移除之前,React 將會執行您的 cleanup 函式。

  • 可選 dependenciessetup程式碼中所有被參考的反應值的列表。反應值包括 props、狀態,以及所有直接在您的元件主體中宣告的變數和函式。如果您的程式碼檢查器有針對 React 進行設定,它將會驗證每個反應值是否都被正確地指定為依賴項。依賴項列表必須具有固定數量的項目,並且必須像[dep1, dep2, dep3]這樣寫在同一行。React 將使用 Object.is 比較來比較每個依賴項与其先前的值。如果您省略此參數,您的 Effect 將在每次元件重新渲染後重新執行。

回傳值

useLayoutEffect 會回傳 undefined

注意事項

  • useLayoutEffect 是一個 Hook,所以你只能在組件的頂層或你自定義的 Hook 中呼叫它。你不能在迴圈或條件式中呼叫它。如果你需要這樣做,請提取一個組件並將 Effect 移到那裡。

  • 當嚴格模式開啟時,React 會在第一次實際設定之前額外執行一次僅限開發的設定+清理週期。這是一個壓力測試,用於確保你的清理邏輯與你的設定邏輯「鏡像」,並且它會停止或撤銷設定正在執行的任何操作。如果這造成問題,請實作清理函式。

  • 如果你的某些相依性是在組件內定義的物件或函式,則存在它們會導致 Effect 比需要更頻繁地重新執行的風險。要解決此問題,請移除不必要的物件函式相依性。你也可以將狀態更新非反應式邏輯提取到 Effect 之外。

  • Effects 僅在客戶端執行。它們在伺服器渲染期間不執行。

  • useLayoutEffect 內部的程式碼以及從中排定的所有狀態更新都會阻止瀏覽器重新繪製畫面。過度使用會導致應用程式變慢。如果可能,請盡量使用 useEffect

  • 如果你在 useLayoutEffect 內觸發狀態更新,React 將立即執行所有剩餘的 Effects,包括 useEffect


用法

在瀏覽器重新繪製畫面之前測量佈局

大多數組件不需要知道它們在螢幕上的位置和大小來決定要渲染什麼。它們只回傳一些 JSX。然後瀏覽器計算它們的*佈局*(位置和大小)並重新繪製畫面。

有時,這是不夠的。想像一個在滑鼠懸停時出現在某個元素旁邊的工具提示。如果有足夠的空間,工具提示應該出現在元素上方,但如果它不適合,它應該出現在下方。為了在正確的最終位置渲染工具提示,你需要知道它的高度(即它是否適合放在頂部)。

要做到這一點,你需要分兩步渲染

  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 元件必須渲染兩次(第一次將 tooltipHeight 初始化為 0,然後使用實際測量的高度),您只會看到最終結果。這就是為什麼在此範例中需要使用 useLayoutEffect 而不是 useEffect 的原因。讓我們在下方詳細了解差異。

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
  );
}

注意事項

兩次渲染並阻止瀏覽器會損害效能。盡可能避免這種情況。


疑難排解

我收到錯誤訊息:「useLayoutEffect 在伺服器上無作用」

useLayoutEffect 的目的是讓您的元件使用佈局資訊進行渲染:

  1. 渲染初始內容。
  2. 在瀏覽器重新繪製螢幕*之前*測量佈局。
  3. 使用您讀取的佈局資訊渲染最終內容。

當您或您的框架使用伺服器端渲染時,您的 React 應用程式會在伺服器上渲染為 HTML 以進行初始渲染。這讓您可以在 JavaScript 程式碼載入之前顯示初始 HTML。

問題是在伺服器上沒有佈局資訊。

先前的範例中,Tooltip 元件中的 useLayoutEffect 呼叫讓它可以根據內容高度正確定位自身(在內容上方或下方)。如果您嘗試將 Tooltip 作為初始伺服器 HTML 的一部分進行渲染,則無法確定這一點。在伺服器上,還沒有佈局!因此,即使您在伺服器上渲染它,它的位置也會在 JavaScript 載入並執行後在客戶端上「跳動」。

通常,依賴佈局資訊的元件不需要在伺服器上渲染。例如,在初始渲染期間顯示 Tooltip 可能沒有意義。它是由客戶端互動觸發的。

但是,如果您遇到這個問題,您有幾個不同的選項

  • useLayoutEffect 替換為 useEffect。這會告訴 React,可以在不阻止繪製的情況下顯示初始渲染結果(因為原始 HTML 會在您的 Effect 執行之前顯示)。

  • 或者,將您的元件標記為僅限客戶端。這會告訴 React 在伺服器端渲染期間,將其內容替換為最接近的 <Suspense> 邊界,並使用載入後備內容(例如,旋轉器或微光)。

  • 或者,您可以在 hydration 之後才使用 useLayoutEffect 渲染元件。保留一個初始化為 false 的布林值 isMounted 狀態,並在 useEffect 呼叫內將其設定為 true。然後,您的渲染邏輯可以類似於 return isMounted ? <RealContent /> : <FallbackContent />。在伺服器上以及 hydration 期間,使用者將看到 FallbackContent,它不應該呼叫 useLayoutEffect。然後 React 會將其替換為僅在客戶端執行的 RealContent,並且可以包含 useLayoutEffect 呼叫。

  • 如果您將元件與外部資料存放區同步,並且依賴 useLayoutEffect 的原因不是為了測量佈局,請考慮改用 useSyncExternalStore,它支援伺服器端渲染