反應副作用的生命週期

Effects 的生命週期與元件不同。元件可以掛載、更新或卸載。Effect 只能做兩件事:開始同步某些東西,以及稍後停止同步它。如果你的 Effect 依賴於隨著時間推移而變化的屬性和狀態,則此循環可能會發生多次。React 提供了一個 linter 規則來檢查你是否已正確指定 Effect 的依賴項。這可讓你的 Effect 與最新的屬性和狀態保持同步。

你將學到

  • Effect 的生命週期與元件的生命週期有何不同
  • 如何獨立思考每個 Effect
  • 你的 Effect 何時需要重新同步,以及原因
  • 如何確定你的 Effect 的依賴項
  • 值的反應性是什麼意思
  • 空的依賴項陣列是什麼意思
  • React 如何使用 linter 驗證你的依賴項是否正確
  • 當你不同意 linter 時該怎麼辦

Effect 的生命週期

每個 React 元件都會經歷相同的生命週期

  • 元件在新增到螢幕時會被*掛載*。
  • 元件在收到新的屬性或狀態時會*更新*,通常是為了響應互動。
  • 元件在從螢幕移除時會被*卸載*。

這是思考元件的好方法,但*不是*思考 Effects 的好方法。 相反,請嘗試獨立於元件的生命週期來思考每個 Effect。Effect 描述了如何將外部系統與目前的屬性和狀態*同步*。隨著程式碼的更改,同步將需要或多或少地發生。

為了說明這一點,請考慮將你的元件連接到聊天伺服器的 Effect

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

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

你的 Effect 的主體指定了如何**開始同步:**

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

你的 Effect 返回的清除函數指定了如何**停止同步:**

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

直覺上,你可能會認為 React 會在元件掛載時**開始同步**,並在元件卸載時**停止同步**。然而,這並不是故事的結局!有時,在元件保持掛載的同時,可能還需要**多次啟動和停止同步**。

讓我們看看*為什麼*這是必要的,*何時*會發生,以及*如何*控制這種行為。

備註

有些 Effects 根本不返回清除函數。通常情況下,你會想要返回一個函數,但如果你不返回,React 的行為就如同你返回了一個空的清除函數。

為什麼同步可能需要發生多次

想像一下,這個 ChatRoom 元件接收一個使用者在下拉選單中選擇的 roomId 屬性。假設最初使用者選擇 "general" 房間作為 roomId。你的應用程式會顯示 "general" 聊天室

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

function ChatRoom({ roomId /* "general" */ }) {
// ...
return <h1>Welcome to the {roomId} room!</h1>;
}

顯示 UI 後,React 將運行你的 Effect 以**開始同步**。它會連接到 "general" 房間

function ChatRoom({ roomId /* "general" */ }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // Connects to the "general" room
connection.connect();
return () => {
connection.disconnect(); // Disconnects from the "general" room
};
}, [roomId]);
// ...

到目前為止,一切都很好。

稍後,使用者在下拉選單中選擇了不同的房間(例如,"travel")。首先,React 將更新 UI

function ChatRoom({ roomId /* "travel" */ }) {
// ...
return <h1>Welcome to the {roomId} room!</h1>;
}

想想接下來應該發生什麼。使用者看到 "travel" 是 UI 中選定的聊天室。但是,上次運行的 Effect 仍然連接到 "general" 房間。 roomId 屬性已更改,因此你的 Effect 之前執行的操作(連接到 "general" 房間)不再與 UI 匹配。

此時,你希望 React 做兩件事

  1. 停止與舊的 roomId 同步(斷開與 "general" 房間的連接)
  2. 開始與新的 roomId 同步(連接到 "travel" 房間)

幸運的是,你已經教會 React 如何做這兩件事了! 你的 Effect 的主體指定了如何開始同步,而你的清除函數指定了如何停止同步。React 現在需要做的就是以正確的順序並使用正確的屬性和狀態來呼叫它們。讓我們看看它是如何發生的。

React 如何重新同步你的 Effect

回想一下,你的 `ChatRoom` 元件收到了其 `roomId` prop 的新值。它以前是 `"general"`,現在是 `"travel"`。React 需要重新同步你的 Effect 以將你重新連接到不同的房間。

為了**停止同步**,React 會在連接到 `"general"` 房間後呼叫你的 Effect 返回的清除函式。由於 `roomId` 是 `"general"`,清除函式會斷開與 `"general"` 房間的連接。

function ChatRoom({ roomId /* "general" */ }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // Connects to the "general" room
connection.connect();
return () => {
connection.disconnect(); // Disconnects from the "general" room
};
// ...

然後 React 會執行你在這次渲染期間提供的 Effect。這次,`roomId` 是 `"travel"`,因此它會**開始同步**到 `"travel"` 聊天室(直到最終也呼叫其清除函式)。

function ChatRoom({ roomId /* "travel" */ }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // Connects to the "travel" room
connection.connect();
// ...

有了這個機制,你現在連接到使用者在 UI 中選擇的同一個房間。避免了災難!

每次你的元件使用不同的 `roomId` 重新渲染後,你的 Effect 都會重新同步。例如,假設使用者將 `roomId` 從 `"travel"` 更改為 `"music"`。React 將再次透過呼叫其清除函式(將你與 `"travel"` 房間斷開連接)來**停止同步**你的 Effect。然後它將使用新的 `roomId` prop 執行其主體(將你連接到 `"music"` 房間)來再次**開始同步**。

最後,當使用者進入不同的畫面時,`ChatRoom` 卸載。現在根本不需要保持連線。React 將最後一次**停止同步**你的 Effect,並將你與 `"music"` 聊天室斷開連接。

從 Effect 的角度思考

讓我們從 `ChatRoom` 元件的角度回顧一下發生的一切。

  1. `ChatRoom` 掛載,`roomId` 設定為 `"general"`
  2. `ChatRoom` 更新,`roomId` 設定為 `"travel"`
  3. `ChatRoom` 更新,`roomId` 設定為 `"music"`
  4. `ChatRoom` 卸載

在元件生命週期的每個這些時間點,你的 Effect 都做了不同的事情。

  1. 你的 Effect 連接到 `"general"` 房間。
  2. 你的 Effect 與 `"general"` 房間斷開連接,並連接到 `"travel"` 房間。
  3. 你的 Effect 與 `"travel"` 房間斷開連接,並連接到 `"music"` 房間。
  4. 你的 Effect 與 `"music"` 房間斷開連接。

現在讓我們從 Effect 本身的角度來思考發生了什麼。

useEffect(() => {
// Your Effect connected to the room specified with roomId...
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
// ...until it disconnected
connection.disconnect();
};
}, [roomId]);

這段程式碼的結構可能會啟發你將發生的事件視為一系列不重疊的時間段。

  1. 你的 Effect 連接到 `"general"` 房間(直到它斷開連接)。
  2. 你的 Effect 連接到 `"travel"` 房間(直到它斷開連接)。
  3. 你的 Effect 連接到 `"music"` 房間(直到它斷開連接)。

先前,你是從元件的角度思考。當你從元件的角度看待時,很容易將 Effect 想成是在特定時間(例如「渲染後」或「卸載前」)觸發的「回呼」或「生命週期事件」。這種思考方式很快就會變得複雜,所以最好避免。

相反地,請始終一次專注於一個啟動/停止週期。元件是掛載、更新還是卸載都無關緊要。你只需要描述如何開始同步以及如何停止它。如果你做得很好,你的 Effect 將能夠在需要時多次啟動和停止,並保持穩定運作。

這可能會讓你聯想到,當你編寫用於建立 JSX 的渲染邏輯時,你並不會考慮元件是掛載還是更新。你描述螢幕上應該顯示的內容,React 會處理剩下的事情

React 如何驗證你的 Effect 可以重新同步

這裡有一個你可以操作的線上範例。按下「開啟聊天」來掛載 `ChatRoom` 元件。

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

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

function ChatRoom({ roomId }) {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]);
  return <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} />}
    </>
  );
}

請注意,當元件第一次掛載時,你會看到三個日誌訊息。

  1. `✅ 連接到 https://127.0.0.1:1234 的 "general" 房間...` *(僅限開發環境)*
  2. `❌ 與 https://127.0.0.1:1234 的 "general" 房間斷開連接。` *(僅限開發環境)*
  3. ✅ 連接到 https://127.0.0.1:1234 的 "general" 房間...

前兩個日誌訊息僅限開發環境。在開發中,React 總是會將每個元件重新掛載一次。

**React 透過強制在開發環境中立即執行重新同步來驗證你的 Effect 是否可以重新同步。**這可能會讓你聯想到開啟和關閉門一次,以檢查門鎖是否正常運作。React 在開發環境中會額外啟動和停止你的 Effect 一次,以檢查你是否已妥善實作其清除功能

在實際情況中,你的 Effect 會重新同步的主要原因是它使用的某些資料發生了變化。在上面的沙盒中,更改選定的聊天室。請注意,當 roomId 變更時,你的 Effect 會重新同步。

然而,也有一些更不尋常的情況需要重新同步。例如,嘗試在聊天開啟時編輯上面沙盒中的 serverUrl。請注意,Effect 如何響應你對程式碼的編輯而重新同步。未來,React 可能會新增更多依賴重新同步的功能。

React 如何知道它需要重新同步 Effect

你可能會好奇 React 如何知道在 roomId 變更後需要重新同步你的 Effect。這是因為 *你告訴了 React* 它的程式碼依賴於 roomId,方法是將它包含在依賴項列表中:

function ChatRoom({ roomId }) { // The roomId prop may change over time
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // This Effect reads roomId
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId]); // So you tell React that this Effect "depends on" roomId
// ...

這是它的運作方式

  1. 你知道 roomId 是一個 prop,這表示它會隨著時間而改變。
  2. 你知道你的 Effect 讀取了 roomId(因此它的邏輯依賴於一個之後可能會改變的值)。
  3. 這就是為什麼你將它指定為 Effect 的依賴項(以便在 roomId 變更時重新同步)。

每次你的元件重新渲染後,React 都會查看你傳遞的依賴項陣列。如果陣列中的任何值與你在前一次渲染期間傳遞的相同位置的值不同,React 將重新同步你的 Effect。

例如,如果你在初始渲染期間傳遞了 ["general"],然後在下一次渲染期間傳遞了 ["travel"],React 將比較 "general""travel"。這些是不同的值(與Object.is比較),因此 React 將重新同步你的 Effect。另一方面,如果你的元件重新渲染但 roomId 沒有改變,你的 Effect 將保持連接到同一個房間。

每個 Effect 代表一個單獨的同步過程

避免僅僅因為此邏輯需要與你已編寫的 Effect 同時運行,就將不相關的邏輯添加到你的 Effect 中。例如,假設你希望在使用者訪問房間時發送分析事件。你已經有一個依賴於 roomId 的 Effect,因此你可能會想要在那裡添加分析呼叫

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

但想像一下,你稍後會在此 Effect 中新增另一個需要重新建立連線的依賴項。如果此 Effect 重新同步,它也會為同一個房間呼叫 logVisit(roomId),這不是你的本意。記錄訪問次數**是一個與連線不同的過程**。將它們寫成兩個單獨的 Effect

function ChatRoom({ roomId }) {
useEffect(() => {
logVisit(roomId);
}, [roomId]);

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

你程式碼中的每個 Effect 都應該代表一個單獨且獨立的同步過程。

在上面的範例中,刪除一個 Effect 不會破壞另一個 Effect 的邏輯。這很好地表明它們同步不同的東西,因此將它們分開是有意義的。另一方面,如果你將一個 cohesive 的邏輯片段拆分為單獨的 Effect,程式碼可能看起來更「乾淨」,但會更難維護。這就是為什麼你應該考慮這些過程是相同還是不同,而不是程式碼看起來是否更乾淨。

Effects 「反應」 反應值

你的 Effect 讀取了兩個變數(serverUrlroomId),但你只指定了 roomId 作為依賴項

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

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

為什麼 serverUrl 不需要作為依賴項?

這是因為 serverUrl 永遠不會因為重新渲染而改變。無論元件重新渲染多少次,它始終相同。由於 serverUrl 永遠不會改變,因此將其指定為依賴項是沒有意義的。畢竟,依賴項只在它們隨著時間改變時才會起作用!

另一方面,roomId 在重新渲染時可能會不同。**在元件內部宣告的 Props、狀態和其他值是*反應性的*,因為它們是在渲染期間計算的,並且參與 React 資料流。**

如果 serverUrl 是一個狀態變數,它將是反應性的。反應值必須包含在依賴項中

function ChatRoom({ roomId }) { // Props change over time
const [serverUrl, setServerUrl] = useState('https://127.0.0.1:1234'); // State may change over time

useEffect(() => {
const connection = createConnection(serverUrl, roomId); // Your Effect reads props and state
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId, serverUrl]); // So you tell React that this Effect "depends on" on props and state
// ...
}

通過將 serverUrl 包含為依賴項,你可以確保 Effect 在它更改後重新同步。

嘗試更改選定的聊天室或在此沙盒中編輯伺服器 URL

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

每當你更改反應值(例如 roomIdserverUrl)時,Effect 都會重新連接到聊天伺服器。

具有空依賴項的 Effect 的含義

如果你將 serverUrlroomId 都移到元件外部會發生什麼?

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

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

現在你的 Effect 的程式碼沒有使用*任何*反應值,因此它的依賴項可以為空([])。

從元件的角度來看,空的 [] 依賴項陣列表示此 Effect 僅在元件掛載時連接到聊天室,並且僅在元件卸載時斷開連線。(請記住,React 在開發過程中仍然會額外重新同步一次,以對你的邏輯進行壓力測試。)

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

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

function ChatRoom() {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => connection.disconnect();
  }, []);
  return <h1>Welcome to the {roomId} room!</h1>;
}

export default function App() {
  const [show, setShow] = useState(false);
  return (
    <>
      <button onClick={() => setShow(!show)}>
        {show ? 'Close chat' : 'Open chat'}
      </button>
      {show && <hr />}
      {show && <ChatRoom />}
    </>
  );
}

然而,如果您從 Effect 的角度思考(從 Effect 的角度思考),您根本不需要考慮掛載和卸載。重要的是您已指定 Effect 如何開始和停止同步。目前,它沒有任何反應性依賴項。但如果您希望使用者之後能夠更改 roomIdserverUrl(並且它們會變成反應性的),您的 Effect 的程式碼將不會改變。您只需要將它們添加到依賴項中即可。

所有在組件主體中宣告的變數都是反應性的

屬性和狀態並非唯一的反應性值。您從它們計算出的值也具有反應性。如果屬性或狀態發生變化,您的組件將重新渲染,並且從它們計算出的值也將發生變化。這就是為什麼 Effect 使用的所有組件主體中的變數都應該在 Effect 依賴項列表中的原因。

假設使用者可以在下拉選單中選擇聊天伺服器,但他們也可以在設定中設定預設伺服器。假設您已經將設定狀態放入 Context 中,以便您從該 Context 中讀取 settings。現在,您可以根據從屬性中選擇的伺服器和預設伺服器來計算 serverUrl

function ChatRoom({ roomId, selectedServerUrl }) { // roomId is reactive
const settings = useContext(SettingsContext); // settings is reactive
const serverUrl = selectedServerUrl ?? settings.defaultServerUrl; // serverUrl is reactive
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // Your Effect reads roomId and serverUrl
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId, serverUrl]); // So it needs to re-synchronize when either of them changes!
// ...
}

在此範例中,serverUrl 不是屬性或狀態變數。它是您在渲染期間計算的常規變數。但它是在渲染期間計算的,因此它可能會因重新渲染而改變。這就是為什麼它具有反應性的原因。

組件內的所有值(包括屬性、狀態和組件主體中的變數)都具有反應性。任何反應性值都可能在重新渲染時發生變化,因此您需要將反應性值包含為 Effect 的依賴項。

換句話說,Effect 會對組件主體中的所有值「做出反應」。

深入探討

全域或可變值可以作為依賴項嗎?

可變值(包括全域變數)不具有反應性。

location.pathname 這樣的可變值不能作為依賴項。它是可變的,因此它可以隨時在 React 渲染資料流之外完全更改。更改它不會觸發組件的重新渲染。因此,即使您在依賴項中指定了它,React 也*不會知道*在它更改時重新同步 Effect。這也違反了 React 的規則,因為在渲染期間(也就是計算依賴項時)讀取可變資料會破壞渲染的純度。相反地,您應該使用 useSyncExternalStore 讀取和訂閱外部可變值。

ref.current 這樣的可變值,或者您從中讀取的內容也不能作為依賴項。 useRef 返回的 ref 物件本身可以作為依賴項,但其 current 屬性是刻意設計為可變的。它允許您在不觸發重新渲染的情況下追蹤某些內容。但是由於更改它不會觸發重新渲染,因此它不是反應性值,React 不會知道在它更改時重新執行您的 Effect。

正如您將在本頁下方學到的,linter 會自動檢查這些問題。

React 會驗證您是否已將每個反應性值指定為依賴項

如果您的 linter 已設定為 React,它將檢查 Effect 程式碼使用的每個反應性值是否都已宣告為其依賴項。例如,這是一個 lint 錯誤,因為 roomIdserverUrl 都具有反應性

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

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

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => connection.disconnect();
  }, []); // <-- Something's wrong here!

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

這看起來像是一個 React 錯誤,但實際上 React 指出的是您程式碼中的一個錯誤。roomIdserverUrl 都可能會隨著時間而改變,但您忘記了在它們改變時重新同步您的 Effect。即使在使用者在 UI 中選擇不同的值之後,您仍將保持連線到初始的 roomIdserverUrl

要修復此錯誤,請按照 linter 的建議,將 roomIdserverUrl 指定為 Effect 的依賴項

function ChatRoom({ roomId }) { // roomId is reactive
const [serverUrl, setServerUrl] = useState('https://127.0.0.1:1234'); // serverUrl is reactive
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [serverUrl, roomId]); // ✅ All dependencies declared
// ...
}

在上面的沙盒中嘗試此修復方法。驗證 linter 錯誤是否已消失,以及聊天是否在需要時重新連線。

備註

在某些情況下,即使值是在元件內部宣告的,React 也*知道*該值永遠不會改變。例如,從 useState 返回的 set 函式 以及由 useRef 返回的 ref 物件都是*穩定*的——它們保證在重新渲染時不會改變。穩定的值是非反應式的,因此您可以將它們從列表中省略。包含它們也是允許的:它們不會改變,所以沒關係。

當您不想重新同步時該怎麼辦

在前一個例子中,您已通過將 roomIdserverUrl 列為依賴項來修復程式碼檢查錯誤。

但是,您可以改為向程式碼檢查器“證明”這些值不是反應式值,即它們*不能*因為重新渲染而改變。例如,如果 serverUrlroomId 不依賴於渲染並且始終具有相同的值,則可以將它們移到元件外部。現在它們不需要作為依賴項。

const serverUrl = 'https://127.0.0.1:1234'; // serverUrl is not reactive
const roomId = 'general'; // roomId is not reactive

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

您也可以將它們移到*Effect 內部。*它們不是在渲染期間計算的,因此它們不是反應式的。

function ChatRoom() {
useEffect(() => {
const serverUrl = 'https://127.0.0.1:1234'; // serverUrl is not reactive
const roomId = 'general'; // roomId is not reactive
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, []); // ✅ All dependencies declared
// ...
}

Effects 是反應式的程式碼塊。當您在其中讀取的值發生變化時,它們會重新同步。與每次互動只運行一次的事件處理程式不同,Effects 會在需要同步時運行。

您不能“選擇”您的依賴項。您的依賴項必須包含您在 Effect 中讀取的每個反應式值。程式碼檢查器會強制執行此操作。有時這可能會導致無限迴圈等問題,並導致您的 Effect 過於頻繁地重新同步。不要通過抑制程式碼檢查器來解決這些問題!您可以嘗試以下方法。

陷阱

程式碼檢查器是您的朋友,但它的功能有限。程式碼檢查器只知道依賴項何時*錯誤*。它不知道解決每個案例的*最佳*方法。如果程式碼檢查器建議一個依賴項,但新增它會導致迴圈,這並不意味著應該忽略程式碼檢查器。您需要更改 Effect 內部(或外部)的程式碼,以便該值不是反應式的,並且*不需要*作為依賴項。

如果您有一個現有的程式碼庫,您可能有一些像這樣抑制程式碼檢查器的 Effects。

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

下一中,您將學習如何在不違反規則的情況下修復此程式碼。修復它始終是值得的!

重點回顧

  • 元件可以掛載、更新和卸載。
  • 每個 Effect 都有與周圍元件不同的生命週期。
  • 每個 Effect 描述一個可以*啟動*和*停止*的單獨同步過程。
  • 當您編寫和讀取 Effects 時,請從每個 Effect 的角度(如何啟動和停止同步)而不是從元件的角度(如何掛載、更新或卸載)來思考。
  • 在元件主體內宣告的值是“反應式”的。
  • 反應式值應該重新同步 Effect,因為它們會隨著時間而改變。
  • 程式碼檢查器會驗證 Effect 內部使用的所有反應式值是否都被指定為依賴項。
  • 程式碼檢查器標記的所有錯誤都是合法的。始終有辦法修復程式碼以不違反規則。

挑戰 1 5:
修復每次按鍵都重新連線的問題

在此範例中,ChatRoom 元件會在元件掛載時連線到聊天室,在卸載時斷開連線,並在您選擇不同的聊天室時重新連線。此行為是正確的,因此您需要保持它的運作。

但是,有一個問題。每當您在底部的訊息方塊輸入中輸入時,ChatRoom 也會重新連線到聊天室。(您可以通過清除主控台並在輸入中輸入來注意到這一點。)修復此問題,使其不再發生。

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 connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => connection.disconnect();
  });

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