useEffect 是一個 React Hook,可讓您將元件與外部系統同步。

useEffect(setup, dependencies?)

參考

useEffect(setup, dependencies?)

在元件的最上層呼叫 useEffect 來宣告一個 Effect

import { useEffect } from 'react';
import { createConnection } from './chat.js';

function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://127.0.0.1:1234');

useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [serverUrl, roomId]);
// ...
}

請參閱下面的更多範例。

參數

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

  • 選用 dependencies:在 setup程式碼中引用的所有反應值的清單。反應值包括 props、狀態,以及直接在元件主體內宣告的所有變數和函式。如果您的程式碼檢查器已針對 React 進行設定,它將驗證每個反應值是否已正確指定為依賴項。依賴項清單必須具有固定數量的項目,並且像 [dep1, dep2, dep3] 一樣內嵌編寫。 React 將使用 Object.is 比較將每個依賴項与其先前值進行比較。如果您省略此參數,則在每次重新渲染元件後,您的 Effect 都將重新執行。查看傳遞依賴項陣列、空陣列和完全沒有依賴項之間的差異。

回傳值

useEffect 回傳 undefined

注意事項

  • useEffect 是一個 Hook,所以你只能在組件的最上層或你自己的 Hooks 中呼叫它。你不能在迴圈或條件式中呼叫它。如果你需要這樣做,請提取一個新的組件並將狀態移入其中。

  • 如果你沒有嘗試與某些外部系統同步,你可能不需要 Effect。

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

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

  • 如果你的 Effect 不是由互動(例如點擊)引起的,React 通常會讓瀏覽器先繪製更新的畫面,然後再執行你的 Effect。如果你的 Effect 正在執行某些視覺操作(例如,定位工具提示),並且延遲很明顯(例如,它會閃爍),請將 useEffect 替換為useLayoutEffect

  • 如果你的 Effect 是由互動(例如點擊)引起的,React 可能會在瀏覽器繪製更新的畫面之前執行你的 Effect。這確保事件系統可以觀察到 Effect 的結果。通常,這會按預期工作。但是,如果你必須將工作延遲到繪製之後,例如 alert(),則可以使用 setTimeout。請參閱reactwg/react-18/128 以獲取更多資訊。

  • 即使你的 Effect 是由互動(例如點擊)引起的,React 也可能允許瀏覽器在處理 Effect 內的狀態更新之前重新繪製畫面。通常,這會按預期工作。但是,如果你必須阻止瀏覽器重新繪製畫面,則需要將 useEffect 替換為useLayoutEffect

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


用法...

連接到外部系統...

某些組件需要在頁面上顯示時保持與網路、某些瀏覽器 API 或第三方函式庫的連接。這些系統不受 React 控制,因此它們被稱為*外部系統*。

要將你的組件連接到某些外部系統,請在組件的最上層呼叫 useEffect

import { useEffect } from 'react';
import { createConnection } from './chat.js';

function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://127.0.0.1:1234');

useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [serverUrl, roomId]);
// ...
}

你需要傳遞兩個參數給 useEffect

  1. 一個帶有設定程式碼的*設定函式*,用於連接到該系統。
    • 它應該返回一個帶有清除程式碼的*清除函式*,用於從該系統斷開連接。
  2. 一個dependencies 列表,其中包含在這些函式內使用的組件中的每個值。

React 會在必要時呼叫你的設定和清除函式,這可能會發生多次

  1. 當你的組件被添加到頁面時(掛載),你的設定程式碼會運行。
  2. 在組件的每次重新渲染之後,如果dependencies 發生了變化
    • 首先,你的清除程式碼會使用舊的 props 和 state 運行。
    • 然後,你的設定程式碼會使用新的 props 和 state 運行。
  3. 在你的組件從頁面中移除後(卸載),你的清除程式碼會最後運行一次。

讓我們用上面的例子來說明這個順序。

當上面的 ChatRoom 組件被添加到頁面時,它將使用初始的 serverUrlroomId 連接到聊天室。如果 serverUrlroomId 因重新渲染而更改(例如,如果用戶在下拉選單中選擇了不同的聊天室),你的 Effect 將*斷開與前一個房間的連接,並連接到下一個房間。*當 ChatRoom 組件從頁面中移除時,你的 Effect 將最後斷開一次連接。

為了協助您找出錯誤,在開發過程中,React 會在 設定 之前額外執行一次 設定清除這是一個壓力測試,用於驗證您的 Effect 邏輯是否正確實作。如果這造成明顯的問題,表示您的清除函式缺少一些邏輯。清除函式應該停止或撤銷設定函式所做的任何操作。經驗法則是使用者不應區分設定被呼叫一次(如在正式環境中)和 _設定_ → _清除_ → _設定_ 序列(如在開發環境中)。參考常見的解決方案。

嘗試將每個 Effect 寫成一個獨立的流程,並一次考慮一個設定/清除週期。您的元件是掛載、更新還是卸載都無關緊要。當您的清除邏輯正確地「反映」設定邏輯時,您的 Effect 就能夠在需要時頻繁地執行設定和清除。

備註

Effect 讓您可以讓您的元件與某些外部系統(例如聊天服務)保持同步。這裡的 _外部系統_ 指的是任何不受 React 控制的程式碼,例如:

如果您沒有連接到任何外部系統,您可能不需要 Effect。

連接到外部系統的範例

連接到聊天伺服器的範例 1 5:
連接到聊天伺服器

在此範例中,ChatRoom 元件使用 Effect 與 chat.js 中定義的外部系統保持連線。按下「開啟聊天」以顯示 ChatRoom 元件。此沙盒在開發模式下執行,因此會有一個額外的連線和斷線週期,如這裡所述。嘗試使用下拉式選單和輸入欄位變更 roomIdserverUrl,並查看 Effect 如何重新連接到聊天。按下「關閉聊天」以查看 Effect 最後一次斷線。

import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';

function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://127.0.0.1:1234');

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => {
      connection.disconnect();
    };
  }, [roomId, serverUrl]);

  return (
    <>
      <label>
        Server URL:{' '}
        <input
          value={serverUrl}
          onChange={e => setServerUrl(e.target.value)}
        />
      </label>
      <h1>Welcome to the {roomId} room!</h1>
    </>
  );
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  const [show, setShow] = useState(false);
  return (
    <>
      <label>
        Choose the chat room:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">general</option>
          <option value="travel">travel</option>
          <option value="music">music</option>
        </select>
      </label>
      <button onClick={() => setShow(!show)}>
        {show ? 'Close chat' : 'Open chat'}
      </button>
      {show && <hr />}
      {show && <ChatRoom roomId={roomId} />}
    </>
  );
}


將 Effects 包裝在自訂 Hooks 中

Effects 是一個「逃生艙口」:當您需要「跳出 React」且沒有更好的內建解決方案適用於您的使用案例時,您可以使用它們。如果您發現自己經常需要手動編寫 Effects,通常表示您需要為元件所依賴的常見行為提取一些自訂 Hooks

例如,這個 useChatRoom 自訂 Hook 將 Effect 的邏輯「隱藏」在更具宣告性的 API 後面

function useChatRoom({ serverUrl, roomId }) {
useEffect(() => {
const options = {
serverUrl: serverUrl,
roomId: roomId
};
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [roomId, serverUrl]);
}

然後您可以像這樣從任何元件使用它

function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://127.0.0.1:1234');

useChatRoom({
roomId: roomId,
serverUrl: serverUrl
});
// ...

React 生態系統中也有許多適用於各種用途的優秀自訂 Hooks。

深入瞭解將 Effects 包裝在自訂 Hooks 中。

將 Effects 包裝在自訂 Hooks 中的範例

連接到聊天伺服器的範例 1 3:
自訂 useChatRoom Hook

這個範例與先前的其中一個範例相同,但邏輯被提取到一個自訂 Hook。

import { useState } from 'react';
import { useChatRoom } from './useChatRoom.js';

function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://127.0.0.1:1234');

  useChatRoom({
    roomId: roomId,
    serverUrl: serverUrl
  });

  return (
    <>
      <label>
        Server URL:{' '}
        <input
          value={serverUrl}
          onChange={e => setServerUrl(e.target.value)}
        />
      </label>
      <h1>Welcome to the {roomId} room!</h1>
    </>
  );
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  const [show, setShow] = useState(false);
  return (
    <>
      <label>
        Choose the chat room:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">general</option>
          <option value="travel">travel</option>
          <option value="music">music</option>
        </select>
      </label>
      <button onClick={() => setShow(!show)}>
        {show ? 'Close chat' : 'Open chat'}
      </button>
      {show && <hr />}
      {show && <ChatRoom roomId={roomId} />}
    </>
  );
}


控制非 React 元件

有時,您希望將外部系統與組件的某些屬性或狀態同步。

例如,如果您有一個沒有使用 React 撰寫的第三方地圖元件或影片播放器組件,您可以使用 Effect 來呼叫其方法,使其狀態與 React 組件的目前狀態相符。 此 Effect 建立一個在 map-widget.js 中定義的 MapWidget 類別的實例。 當您更改 Map 組件的 zoomLevel 屬性時,Effect 會呼叫類別實例上的 setZoom() 來保持同步。

import { useRef, useEffect } from 'react';
import { MapWidget } from './map-widget.js';

export default function Map({ zoomLevel }) {
  const containerRef = useRef(null);
  const mapRef = useRef(null);

  useEffect(() => {
    if (mapRef.current === null) {
      mapRef.current = new MapWidget(containerRef.current);
    }

    const map = mapRef.current;
    map.setZoom(zoomLevel);
  }, [zoomLevel]);

  return (
    <div
      style={{ width: 200, height: 200 }}
      ref={containerRef}
    />
  );
}

在此範例中,不需要清除函式,因為 MapWidget 類別僅管理傳遞給它的 DOM 節點。 從樹狀結構中移除 Map React 組件後,瀏覽器 JavaScript 引擎會自動對 DOM 節點和 MapWidget 類別實例進行垃圾回收。


使用 Effects 提取資料

您可以使用 Effect 來為您的組件提取資料。 請注意,如果您使用框架,使用框架的資料提取機制將比手動撰寫 Effects 更有效率。

如果您想要從 Effect 手動提取資料,您的程式碼可能如下所示:

import { useState, useEffect } from 'react';
import { fetchBio } from './api.js';

export default function Page() {
const [person, setPerson] = useState('Alice');
const [bio, setBio] = useState(null);

useEffect(() => {
let ignore = false;
setBio(null);
fetchBio(person).then(result => {
if (!ignore) {
setBio(result);
}
});
return () => {
ignore = true;
};
}, [person]);

// ...

請注意,ignore 變數初始化為 false,並在清除期間設定為 true。 這可確保您的程式碼不會受到「競爭條件」的影響:網路回應的到達順序可能與您發送它們的順序不同。

import { useState, useEffect } from 'react';
import { fetchBio } from './api.js';

export default function Page() {
  const [person, setPerson] = useState('Alice');
  const [bio, setBio] = useState(null);
  useEffect(() => {
    let ignore = false;
    setBio(null);
    fetchBio(person).then(result => {
      if (!ignore) {
        setBio(result);
      }
    });
    return () => {
      ignore = true;
    }
  }, [person]);

  return (
    <>
      <select value={person} onChange={e => {
        setPerson(e.target.value);
      }}>
        <option value="Alice">Alice</option>
        <option value="Bob">Bob</option>
        <option value="Taylor">Taylor</option>
      </select>
      <hr />
      <p><i>{bio ?? 'Loading...'}</i></p>
    </>
  );
}

您也可以使用 async / await 語法重寫,但您仍然需要提供清除函式。

import { useState, useEffect } from 'react';
import { fetchBio } from './api.js';

export default function Page() {
  const [person, setPerson] = useState('Alice');
  const [bio, setBio] = useState(null);
  useEffect(() => {
    async function startFetching() {
      setBio(null);
      const result = await fetchBio(person);
      if (!ignore) {
        setBio(result);
      }
    }

    let ignore = false;
    startFetching();
    return () => {
      ignore = true;
    }
  }, [person]);

  return (
    <>
      <select value={person} onChange={e => {
        setPerson(e.target.value);
      }}>
        <option value="Alice">Alice</option>
        <option value="Bob">Bob</option>
        <option value="Taylor">Taylor</option>
      </select>
      <hr />
      <p><i>{bio ?? 'Loading...'}</i></p>
    </>
  );
}

直接在 Effects 中撰寫資料提取會變得重複,並且難以在稍後新增快取和伺服器渲染等優化。 使用自訂 Hook 更容易,無論是您自己的還是社群維護的

深入探討

Effects 中資料提取的良好替代方案是什麼?

在 Effects 內撰寫 fetch 呼叫是提取資料的常見方法,尤其是在純客戶端應用程式中。 然而,這是一種非常手動的方法,並且具有明顯的缺點:

  • Effects 不會在伺服器上執行。 這表示初始伺服器渲染的 HTML 只會包含載入狀態,沒有資料。 用戶端電腦必須下載所有 JavaScript 並渲染您的應用程式,才會發現現在需要載入資料。 這不是很有效率。
  • 直接在 Effects 中提取很容易造成「網路瀑布」。 您渲染父組件,它提取一些資料,渲染子組件,然後它們開始提取它們的資料。 如果網路速度不是很慢,這明顯慢於並行提取所有資料。
  • 直接在 Effects 中提取通常表示您沒有預先載入或快取資料。 例如,如果組件卸載然後再次掛載,它必須再次提取資料。
  • 它不是很符合人體工學。 以不會導致 競爭條件 等錯誤的方式撰寫 fetch 呼叫會涉及相當多的樣板程式碼。

這個缺點清單並非 React 特有。 它適用於使用任何函式庫在掛載時提取資料。 與路由一樣,資料提取並不容易做好,因此我們建議以下方法:

  • 如果您使用 框架,請使用其內建的資料提取機制。 現代 React 框架具有整合的資料提取機制,這些機制效率高且不會遇到上述陷阱。
  • 否則,請考慮使用或建置 client-side 快取。 熱門的開源解決方案包括 React QueryuseSWRReact Router 6.4+。 您也可以建置自己的解決方案,在這種情況下,您會在底層使用 Effects,但也會添加邏輯來重複資料刪除請求、快取回應以及避免網路瀑布(透過預載資料或將資料需求提升到路由)。

如果這些方法都不適合您,您可以繼續直接在 Effects 中擷取資料。


指定反應式依賴項

請注意,您無法「選擇」Effect 的依賴項。 Effect 代碼使用的每個 反應值 都必須宣告為依賴項。 Effect 的依賴項列表由周圍的程式碼決定

function ChatRoom({ roomId }) { // This is a reactive value
const [serverUrl, setServerUrl] = useState('https://127.0.0.1:1234'); // This is a reactive value too

useEffect(() => {
const connection = createConnection(serverUrl, roomId); // This Effect reads these reactive values
connection.connect();
return () => connection.disconnect();
}, [serverUrl, roomId]); // ✅ So you must specify them as dependencies of your Effect
// ...
}

如果 serverUrlroomId 發生變化,您的 Effect 將使用新的值重新連線到聊天室。

反應值 包括 props 以及在組件內部直接宣告的所有變數和函式。 由於 roomIdserverUrl 是反應值,因此您無法將它們從依賴項中移除。如果您嘗試省略它們,並且 您的 linter 已針對 React 正確設定, 則 linter 會將其標記為您需要修復的錯誤

function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://127.0.0.1:1234');

useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, []); // 🔴 React Hook useEffect has missing dependencies: 'roomId' and 'serverUrl'
// ...
}

要移除依賴項,您需要向 linter「證明」它*不需要*成為依賴項。 例如,您可以將 serverUrl 移出您的組件,以證明它不是反應式的,並且在重新渲染時不會改變

const serverUrl = 'https://127.0.0.1:1234'; // Not a reactive value anymore

function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ All dependencies declared
// ...
}

現在,serverUrl 不是反應值(並且在重新渲染時無法更改),它不需要成為依賴項。 如果您的 Effect 的程式碼未使用任何反應值,則其依賴項列表應為空 ([]):

const serverUrl = 'https://127.0.0.1:1234'; // Not a reactive value anymore
const roomId = 'music'; // Not a reactive value anymore

function ChatRoom() {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, []); // ✅ All dependencies declared
// ...
}

具有空依賴項的 Effect 不會在任何組件的 props 或狀態更改時重新執行。

陷阱

如果您有現有的程式碼庫,您可能有一些 Effects 會像這樣抑制 linter

useEffect(() => {
// ...
// 🔴 Avoid suppressing the linter like this:
// eslint-ignore-next-line react-hooks/exhaustive-deps
}, []);

當依賴項與程式碼不符時,引入錯誤的風險很高。 透過抑制 linter,您就對 React「謊稱」您的 Effect 所依賴的值。 請證明它們是不必要的。

傳遞反應式依賴項的範例

連接到聊天伺服器的範例 1 3:
傳遞依賴項陣列

如果您指定依賴項,您的 Effect 將在**初始渲染*和*依賴項更改後的重新渲染之後執行。**

useEffect(() => {
// ...
}, [a, b]); // Runs again if a or b are different

在下面的範例中,serverUrlroomId反應值, 因此它們都必須指定為依賴項。 因此,在下拉式選單中選擇不同的房間或編輯伺服器 URL 輸入會導致聊天重新連線。 但是,由於 message 未在 Effect 中使用(因此它不是依賴項),因此編輯訊息不會重新連線到聊天。

import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';

function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://127.0.0.1:1234');
  const [message, setMessage] = useState('');

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => {
      connection.disconnect();
    };
  }, [serverUrl, roomId]);

  return (
    <>
      <label>
        Server URL:{' '}
        <input
          value={serverUrl}
          onChange={e => setServerUrl(e.target.value)}
        />
      </label>
      <h1>Welcome to the {roomId} room!</h1>
      <label>
        Your message:{' '}
        <input value={message} onChange={e => setMessage(e.target.value)} />
      </label>
    </>
  );
}

export default function App() {
  const [show, setShow] = useState(false);
  const [roomId, setRoomId] = useState('general');
  return (
    <>
      <label>
        Choose the chat room:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">general</option>
          <option value="travel">travel</option>
          <option value="music">music</option>
        </select>
        <button onClick={() => setShow(!show)}>
          {show ? 'Close chat' : 'Open chat'}
        </button>
      </label>
      {show && <hr />}
      {show && <ChatRoom roomId={roomId}/>}
    </>
  );
}


根據 Effect 中的先前狀態更新狀態

當您想要根據 Effect 中的先前狀態更新狀態時,您可能會遇到問題

function Counter() {
const [count, setCount] = useState(0);

useEffect(() => {
const intervalId = setInterval(() => {
setCount(count + 1); // You want to increment the counter every second...
}, 1000)
return () => clearInterval(intervalId);
}, [count]); // 🚩 ... but specifying `count` as a dependency always resets the interval.
// ...
}

由於 count 是一個反應值,因此必須在依賴項列表中指定它。 但是,這會導致 Effect 在每次 count 改變時清除並重新設定。 這並不理想。

要解決此問題,請將 c => c + 1 狀態更新器 傳遞給 setCount

import { useState, useEffect } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const intervalId = setInterval(() => {
      setCount(c => c + 1); // ✅ Pass a state updater
    }, 1000);
    return () => clearInterval(intervalId);
  }, []); // ✅ Now count is not a dependency

  return <h1>{count}</h1>;
}

現在您傳遞的是 c => c + 1 而不是 count + 1您的 Effect 不再需要依賴 count 此修復的結果是,它不需要在每次 count 改變時清除並重新設定間隔。


移除不必要的物件依賴項

如果您的 Effect 依賴於在渲染期間建立的物件或函式,它可能會執行得太頻繁。 例如,此 Effect 在每次渲染後都會重新連線,因為 options 物件在每次渲染時都不同:

const serverUrl = 'https://127.0.0.1:1234';

function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');

const options = { // 🚩 This object is created from scratch on every re-render
serverUrl: serverUrl,
roomId: roomId
};

useEffect(() => {
const connection = createConnection(options); // It's used inside the Effect
connection.connect();
return () => connection.disconnect();
}, [options]); // 🚩 As a result, these dependencies are always different on a re-render
// ...

避免使用在渲染期間建立的物件作為依賴項。 而是應該在 Effect 內部建立物件

import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';

const serverUrl = 'https://127.0.0.1:1234';

function ChatRoom({ roomId }) {
  const [message, setMessage] = useState('');

  useEffect(() => {
    const options = {
      serverUrl: serverUrl,
      roomId: roomId
    };
    const connection = createConnection(options);
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]);

  return (
    <>
      <h1>Welcome to the {roomId} room!</h1>
      <input value={message} onChange={e => setMessage(e.target.value)} />
    </>
  );
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  return (
    <>
      <label>
        Choose the chat room:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">general</option>
          <option value="travel">travel</option>
          <option value="music">music</option>
        </select>
      </label>
      <hr />
      <ChatRoom roomId={roomId} />
    </>
  );
}

現在,您在 Effect 內建立了 options 物件,Effect 本身只依賴於 roomId 字串。

透過此修正,在輸入框中輸入文字不會重新連線聊天。與會重新建立的物件不同,像 roomId 這樣的字串除非您將其設定為另一個值,否則它不會改變。深入了解移除依賴項。


移除不必要的函式依賴項

如果您的 Effect 依賴於渲染期間建立的物件或函式,它可能會過於頻繁地執行。例如,由於 createOptions 函式在每次渲染時都不同,因此此 Effect 在每次渲染後都會重新連線:

function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');

function createOptions() { // 🚩 This function is created from scratch on every re-render
return {
serverUrl: serverUrl,
roomId: roomId
};
}

useEffect(() => {
const options = createOptions(); // It's used inside the Effect
const connection = createConnection();
connection.connect();
return () => connection.disconnect();
}, [createOptions]); // 🚩 As a result, these dependencies are always different on a re-render
// ...

每次重新渲染時從頭開始建立一個函式本身並不是問題。您不需要對此進行優化。但是,如果您將其用作 Effect 的依賴項,則會導致您的 Effect 在每次重新渲染後重新執行。

避免使用渲染期間建立的函式作為依賴項。請改在 Effect 內宣告它。

import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';

const serverUrl = 'https://127.0.0.1:1234';

function ChatRoom({ roomId }) {
  const [message, setMessage] = useState('');

  useEffect(() => {
    function createOptions() {
      return {
        serverUrl: serverUrl,
        roomId: roomId
      };
    }

    const options = createOptions();
    const connection = createConnection(options);
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]);

  return (
    <>
      <h1>Welcome to the {roomId} room!</h1>
      <input value={message} onChange={e => setMessage(e.target.value)} />
    </>
  );
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  return (
    <>
      <label>
        Choose the chat room:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">general</option>
          <option value="travel">travel</option>
          <option value="music">music</option>
        </select>
      </label>
      <hr />
      <ChatRoom roomId={roomId} />
    </>
  );
}

現在,您在 Effect 內定義了 createOptions 函式,Effect 本身只依賴於 roomId 字串。透過此修正,在輸入框中輸入文字不會重新連線聊天。與會重新建立的函式不同,像 roomId 這樣的字串除非您將其設定為另一個值,否則它不會改變。深入了解移除依賴項。


從 Effect 讀取最新的 props 和 state

建構中

本節說明在穩定版 React 中**尚未發布的實驗性 API**。

預設情況下,當您從 Effect 讀取反應值時,您必須將其新增為依賴項。這可確保您的 Effect 能「反應」該值的每次變化。對於大多數依賴項,這就是您想要的行為。

**然而,有時您會希望從 Effect 讀取*最新*的 props 和 state,而不要對它們做出「反應」。** 例如,假設您想要記錄每次頁面訪問時購物車中的商品數量。

function Page({ url, shoppingCart }) {
useEffect(() => {
logVisit(url, shoppingCart.length);
}, [url, shoppingCart]); // ✅ All dependencies declared
// ...
}

**如果您想在每次 url 變更後記錄新的頁面訪問,但如果只有 shoppingCart 變更則*不*記錄,該怎麼辦?** 您不能在不違反反應規則的情況下,從依賴項中排除 shoppingCart。但是,您可以表達您*不希望*一段程式碼對變更做出「反應」,即使它是從 Effect 內部呼叫的。使用 useEffectEvent Hook 宣告一個*Effect 事件*,並將讀取 shoppingCart 的程式碼移到其中。

function Page({ url, shoppingCart }) {
const onVisit = useEffectEvent(visitedUrl => {
logVisit(visitedUrl, shoppingCart.length)
});

useEffect(() => {
onVisit(url);
}, [url]); // ✅ All dependencies declared
// ...
}

**Effect 事件不具反應性,而且必須一律從 Effect 的依賴項中省略。** 這讓您可以在其中放置非反應性程式碼(您可以在其中讀取某些 props 和 state 的最新值)。透過在 onVisit 內讀取 shoppingCart,您可以確保 shoppingCart 不會重新執行您的 Effect。

深入了解 Effect 事件如何讓您區分反應性和非反應性程式碼。


在伺服器和客戶端上顯示不同的內容

如果您的應用程式使用伺服器渲染(直接使用或透過框架使用),您的元件將在兩種不同的環境中渲染。在伺服器上,它會渲染以產生初始 HTML。在客戶端上,React 將再次執行渲染程式碼,以便將您的事件處理程式附加到該 HTML。這就是為什麼,為了讓水合作用,您的初始渲染輸出在客戶端和伺服器上必須相同。

在極少數情況下,您可能需要在客戶端上顯示不同的內容。例如,如果您的應用程式從localStorage讀取某些資料,它不可能在伺服器上執行此操作。以下是您可以如何實作此功能:

function MyComponent() {
const [didMount, setDidMount] = useState(false);

useEffect(() => {
setDidMount(true);
}, []);

if (didMount) {
// ... return client-only JSX ...
} else {
// ... return initial JSX ...
}
}

當應用程式載入時,使用者將看到初始渲染輸出。然後,當它載入並水合後,您的 Effect 將會執行,並將 didMount 設定為 true,觸發重新渲染。這將切換到僅限客戶端的渲染輸出。Effect 不會在伺服器上執行,因此這就是為什麼在初始伺服器渲染期間 didMountfalse 的原因。

請謹慎使用此模式。請記住,連線速度較慢的使用者會看到初始內容相當長的時間(可能長達數秒),因此您不希望對元件的外觀進行突兀的更改。在許多情況下,您可以透過使用 CSS 有條件地顯示不同的內容來避免這種情況。


疑難排解

我的 Effect 在元件掛載時執行兩次

在開發環境中,當啟用嚴格模式時,React 會在實際設定之前額外執行一次設定和清除。

這是一個壓力測試,用於驗證您的 Effect 邏輯是否正確實作。如果這造成可見的問題,則表示您的清除函式缺少一些邏輯。清除函式應該停止或撤銷設定函式所做的任何操作。經驗法則是使用者不應該能夠區分設定被呼叫一次(如同在正式環境中)和設定 → 清除 → 設定序列(如同在開發環境中)。

閱讀更多關於 如何幫助找到錯誤 以及 如何修復您的邏輯


我的 Effect 在每次重新渲染後都會執行

首先,請檢查您是否忘記指定依賴陣列

useEffect(() => {
// ...
}); // 🚩 No dependency array: re-runs after every render!

如果您已指定依賴陣列,但您的 Effect 仍然在迴圈中重新執行,那是因為您的其中一個依賴項在每次重新渲染時都不同。

您可以透過手動將您的依賴項記錄到主控台來除錯此問題

useEffect(() => {
// ..
}, [serverUrl, roomId]);

console.log([serverUrl, roomId]);

然後,您可以在主控台中右鍵點擊來自不同重新渲染的陣列,並為它們選擇「儲存為全域變數」。假設第一個被儲存為 temp1,第二個被儲存為 temp2,您就可以使用瀏覽器主控台來檢查兩個陣列中的每個依賴項是否相同

Object.is(temp1[0], temp2[0]); // Is the first dependency the same between the arrays?
Object.is(temp1[1], temp2[1]); // Is the second dependency the same between the arrays?
Object.is(temp1[2], temp2[2]); // ... and so on for every dependency ...

當您找到每次重新渲染時都不同的依賴項時,您通常可以透過以下其中一種方式修復它

作為最後手段(如果這些方法沒有幫助),請使用 useMemouseCallback(適用於函式)包裝它的建立過程。


我的 Effect 持續在無限迴圈中重新執行

如果您的 Effect 在無限迴圈中執行,則必須滿足以下兩個條件

  • 您的 Effect 正在更新某些狀態。
  • 該狀態導致重新渲染,進而導致 Effect 的依賴項發生變化。

在您開始修復問題之前,請問問自己,您的 Effect 是否正在連接到某些外部系統(例如 DOM、網路、第三方小工具等等)。為什麼您的 Effect 需要設定狀態?它是否與該外部系統同步?或者您是否正在嘗試使用它來管理應用程式的資料流程?

如果沒有外部系統,請考慮 完全移除 Effect 是否可以簡化您的邏輯。

如果您真的在與某些外部系統同步,請思考您的 Effect 應該在何時以及在何種情況下更新狀態。是否有影響元件視覺輸出的變化?如果您需要追蹤一些未被渲染使用的資料,則 ref(它不會觸發重新渲染)可能更合適。驗證您的 Effect 不會過度更新狀態(並觸發重新渲染)。

最後,如果您的 Effect 在正確的時間更新狀態,但仍然存在迴圈,那是因為該狀態更新導致 Effect 的其中一個依賴項發生變化。 閱讀如何除錯依賴項的變化


即使我的元件沒有卸載,我的清除邏輯仍然執行

清除函式不僅在卸載期間執行,而是在每次依賴項更改的重新渲染之前執行。此外,在開發環境中,React 會 在元件掛載後立即額外執行一次設定+清除

如果您有清除程式碼而沒有相對應的設定程式碼,通常表示程式碼有問題

useEffect(() => {
// 🔴 Avoid: Cleanup logic without corresponding setup logic
return () => {
doSomething();
};
}, []);

您的清除邏輯應該與設定邏輯「對稱」,並且應該停止或撤銷設定所做的任何操作

useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [serverUrl, roomId]);

瞭解 Effect 生命週期與元件生命週期的不同之處。


我的 Effect 執行了一些視覺操作,我在它執行之前看到閃爍

如果您的 Effect 必須阻止瀏覽器 繪製畫面,請將 useEffect 替換為 useLayoutEffect。請注意,**絕大多數 Effect 不需要這樣做。** 只有在瀏覽器繪製之前執行 Effect 至關重要的情況下才需要這樣做:例如,在使用者看到工具提示之前測量和定位它。