移除 Effect 的依賴項

當你撰寫 Effect 時,linter 會驗證你是否已將 Effect 讀取的每個反應值(例如 props 和 state)都包含在 Effect 的依賴項列表中。這可確保你的 Effect 與元件的最新 props 和 state 保持同步。不必要的依賴項可能會導致你的 Effect 執行過於頻繁,甚至造成無限迴圈。請遵循本指南來檢查並移除 Effects 中不必要的依賴項。

你將學到

  • 如何修復無限 Effect 依賴迴圈
  • 當你想移除依賴項時該怎麼做
  • 如何在不「反應」值的情況下從 Effect 中讀取值
  • 如何以及為何要避免物件和函式依賴項
  • 為何抑制依賴項 linter 是危險的,以及該怎麼做

依賴項應與程式碼匹配

當你撰寫 Effect 時,你首先要指定如何 啟動和停止 你希望 Effect 執行的任何操作

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

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

然後,如果你將 Effect 依賴項留空([]),linter 會建議正確的依賴項

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();
  }, []); // <-- Fix the mistake here!
  return <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} />
    </>
  );
}

根據 linter 的指示填寫

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

Effects 會「反應」反應值。 由於 roomId 是一個反應值(它會因重新渲染而改變),linter 會驗證你是否已將其指定為依賴項。如果 roomId 收到不同的值,React 將重新同步你的 Effect。這可確保聊天保持連線到所選的房間,並對下拉選單「做出反應」

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');
  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 的依賴項。Effect 程式碼使用的每個 反應值 都必須在你的依賴項列表中宣告。依賴項列表由周圍的程式碼決定

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

function ChatRoom({ roomId }) { // This is a reactive value
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // This Effect reads that reactive value
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ So you must specify that reactive value as a dependency of your Effect
// ...
}

反應值 包括 props 以及所有直接在元件內部宣告的變數和函式。由於 roomId 是一個反應值,你無法將其從依賴項列表中移除。linter 不允許這樣做

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

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

而且 linter 是對的!由於 roomId 可能會隨著時間改變,這會在你的程式碼中引入錯誤。

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

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

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

現在 roomId 不是反應值(並且不會在重新渲染時改變),它不需要成為依賴項

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

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

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

這就是為什麼你現在可以指定一個 空的([])依賴項列表。 你的 Effect *真的不再*依賴任何反應值,所以當元件的任何 props 或 state 改變時,它*真的不需要*重新執行。

要更改依賴項,請更改程式碼

你可能已經注意到你的工作流程中的一個模式

  1. 首先,你更改程式碼你的 Effect 或你的反應值的宣告方式。
  2. 然後,你遵循 linter 並調整依賴項以匹配你已更改的程式碼。
  3. 如果你對依賴項列表不滿意,你回到第一步(並再次更改程式碼)。

最後一部分很重要。如果你想更改依賴項,請先更改周圍的程式碼。 你可以將依賴項列表視為 Effect 程式碼使用的所有反應值的列表。 你不*選擇*要放在該列表上的內容。該列表*描述*你的程式碼。要更改依賴項列表,請更改程式碼。

這可能感覺像在解方程式。您可能從一個目標開始(例如,移除一個依賴項),然後您需要“找到”與該目標匹配的程式碼。並非每個人都覺得解方程式很有趣,對於撰寫 Effects 來說也是如此!幸運的是,您可以嘗試以下列出的常見方法。

陷阱

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

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

當依賴項與程式碼不匹配時,引入錯誤的風險非常高。 通過抑制程式碼檢查器的警告,您向 React“謊報”了您的 Effect 所依賴的值。

請改用以下技巧。

深入探討

為什麼抑制依賴項程式碼檢查器的警告如此危險?

抑制程式碼檢查器的警告會導致非常不直觀的錯誤,這些錯誤難以發現和修復。這裡有一個例子

import { useState, useEffect } from 'react';

export default function Timer() {
  const [count, setCount] = useState(0);
  const [increment, setIncrement] = useState(1);

  function onTick() {
	setCount(count + increment);
  }

  useEffect(() => {
    const id = setInterval(onTick, 1000);
    return () => clearInterval(id);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return (
    <>
      <h1>
        Counter: {count}
        <button onClick={() => setCount(0)}>Reset</button>
      </h1>
      <hr />
      <p>
        Every second, increment by:
        <button disabled={increment === 0} onClick={() => {
          setIncrement(i => i - 1);
        }}></button>
        <b>{increment}</b>
        <button onClick={() => {
          setIncrement(i => i + 1);
        }}>+</button>
      </p>
    </>
  );
}

假設您只想在掛載時執行 Effect。您已經讀到空的 ([]) 依賴項 會做到這一點,所以您決定忽略程式碼檢查器的警告,並強制指定 [] 作為依賴項。

這個計數器應該每秒鐘根據兩個按鈕設定的數量遞增。但是,由於您向 React“謊報”此 Effect 不依賴任何東西,因此 React 會永遠持續使用初始渲染中的 onTick 函式。在該渲染期間, count0increment1。這就是為什麼該渲染中的 onTick 總是每秒呼叫 setCount(0 + 1),而您總是看到 1。當這類錯誤分散在多個組件中時,更難以修復。

總有比忽略程式碼檢查器更好的解決方案!要修復此程式碼,您需要將 onTick 新增到依賴項列表中。(為了確保間隔只設定一次,onTick 設為 Effect 事件。

我們建議將依賴項程式碼檢查器錯誤視為編譯錯誤。如果您不抑制它,您將永遠不會看到這類錯誤。 本頁的其餘部分說明了此情況和其他情況的替代方案。

移除不必要的依賴項

每次調整 Effect 的依賴項以反映程式碼時,請查看依賴項列表。當任何這些依賴項發生變化時,Effect 重新執行是否有意義?有時,答案是“否”

  • 您可能希望在不同條件下重新執行 Effect 的*不同部分*。
  • 您可能只想讀取某些依賴項的*最新值*,而不是“反應”其變化。
  • 依賴項可能會*無意中*過於頻繁地更改,因為它是物件或函式。

要找到正確的解決方案,您需要回答一些關於您的 Effect 的問題。讓我們逐步了解它們。

此程式碼是否應該移至事件處理常式?

您應該考慮的第一件事是這段程式碼是否應該是一個 Effect。

想像一個表單。送出時,您將 submitted 狀態變數設定為 true。您需要傳送 POST 請求並顯示通知。您已將此邏輯放入一個“反應” submittedtrue 的 Effect 中

function Form() {
const [submitted, setSubmitted] = useState(false);

useEffect(() => {
if (submitted) {
// 🔴 Avoid: Event-specific logic inside an Effect
post('/api/register');
showNotification('Successfully registered!');
}
}, [submitted]);

function handleSubmit() {
setSubmitted(true);
}

// ...
}

稍後,您想要根據目前的主題設定通知訊息的樣式,因此您讀取目前的主題。由於 theme 在組件主體中宣告,它是一個反應值,因此您將其新增為依賴項

function Form() {
const [submitted, setSubmitted] = useState(false);
const theme = useContext(ThemeContext);

useEffect(() => {
if (submitted) {
// 🔴 Avoid: Event-specific logic inside an Effect
post('/api/register');
showNotification('Successfully registered!', theme);
}
}, [submitted, theme]); // ✅ All dependencies declared

function handleSubmit() {
setSubmitted(true);
}

// ...
}

這樣做,您就引入了一個錯誤。想像您先送出表單,然後在深色和淺色主題之間切換。theme 將會更改,Effect 將會重新執行,因此它將再次顯示相同的通知!

這裡的問題是,這首先不應該是一個 Effect。 您想要傳送此 POST 請求並顯示*送出表單*的回應通知,這是一個特定互動。要執行一些程式碼來回應特定互動,請將該邏輯直接放入相應的事件處理常式中

function Form() {
const theme = useContext(ThemeContext);

function handleSubmit() {
// ✅ Good: Event-specific logic is called from event handlers
post('/api/register');
showNotification('Successfully registered!', theme);
}

// ...
}

現在程式碼在事件處理常式中,它不再是反應式的,因此它只會在使用者送出表單時執行。詳細了解在事件處理常式和 Effects 之間進行選擇以及如何刪除不必要的 Effects。

您的 Effect 是否正在做幾件無關的事情?

您應該問自己的下一個問題是,您的 Effect 是否正在做幾件無關的事情。

想像您正在建立一個送貨表單,使用者需要在表單中選擇他們的城市和地區。您根據所選的 國家 從伺服器擷取 城市 列表,並將其顯示在下拉式選單中。

function ShippingForm({ country }) {
const [cities, setCities] = useState(null);
const [city, setCity] = useState(null);

useEffect(() => {
let ignore = false;
fetch(`/api/cities?country=${country}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setCities(json);
}
});
return () => {
ignore = true;
};
}, [country]); // ✅ All dependencies declared

// ...

這是 在 Effect 中擷取資料 的一個好例子。您正在根據 國家 屬性,將 城市 狀態與網路同步。您無法在事件處理程式中執行此操作,因為您需要在 ShippingForm 顯示後以及 國家 發生變更時(無論是哪種互動造成)立即擷取資料。

現在,假設您要新增第二個用於選擇城市地區的下拉式選單,它應該會擷取目前所選 城市地區 列表。您可以先在同一個 Effect 中新增第二個 fetch 呼叫來取得地區列表。

function ShippingForm({ country }) {
const [cities, setCities] = useState(null);
const [city, setCity] = useState(null);
const [areas, setAreas] = useState(null);

useEffect(() => {
let ignore = false;
fetch(`/api/cities?country=${country}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setCities(json);
}
});
// 🔴 Avoid: A single Effect synchronizes two independent processes
if (city) {
fetch(`/api/areas?city=${city}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setAreas(json);
}
});
}
return () => {
ignore = true;
};
}, [country, city]); // ✅ All dependencies declared

// ...

然而,由於 Effect 現在使用 城市 狀態變數,因此您必須將 城市 新增到依賴項列表中。反過來,這也引發了一個問題:當使用者選擇不同的城市時,Effect 將會重新執行並呼叫 fetchCities(country)。因此,您將會不必要地多次重新擷取城市列表。

這段程式碼的問題在於您正在同步兩個不同的、不相關的事物。

  1. 您想要根據 國家 屬性將 城市 狀態與網路同步。
  2. 您想要根據 城市 狀態將 地區 狀態與網路同步。

將邏輯拆分為兩個 Effect,每個 Effect 都會對應它需要同步的屬性做出反應。

function ShippingForm({ country }) {
const [cities, setCities] = useState(null);
useEffect(() => {
let ignore = false;
fetch(`/api/cities?country=${country}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setCities(json);
}
});
return () => {
ignore = true;
};
}, [country]); // ✅ All dependencies declared

const [city, setCity] = useState(null);
const [areas, setAreas] = useState(null);
useEffect(() => {
if (city) {
let ignore = false;
fetch(`/api/areas?city=${city}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setAreas(json);
}
});
return () => {
ignore = true;
};
}
}, [city]); // ✅ All dependencies declared

// ...

現在,第一個 Effect 僅在 國家 變更時才會重新執行,而第二個 Effect 則在 城市 變更時重新執行。您已將它們按目的分開:兩個不同的事物由兩個獨立的 Effect 同步。兩個獨立的 Effect 有兩個獨立的依賴項列表,因此它們不會意外地觸發彼此。

最終的程式碼比原來的程式碼長,但拆分這些 Effect 仍然是正確的。每個 Effect 都應該代表一個獨立的同步過程。 在此範例中,刪除一個 Effect 不會破壞另一個 Effect 的邏輯。這表示它們*同步不同的東西,*將它們分開是件好事。如果您擔心程式碼重複,您可以透過 將重複的邏輯提取到自訂 Hook 中 來改進此程式碼。

您是否正在讀取某些狀態來計算下一個狀態?

每當收到新訊息時,這個 Effect 就會使用一個新建立的陣列來更新 messages 狀態變數。

function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
setMessages([...messages, receivedMessage]);
});
// ...

它使用 messages 變數來 建立一個新的陣列,以所有現有的訊息開始,並將新訊息新增到結尾。但是,由於 messages 是一個由 Effect 讀取的反應值,因此它必須是一個依賴項。

function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
setMessages([...messages, receivedMessage]);
});
return () => connection.disconnect();
}, [roomId, messages]); // ✅ All dependencies declared
// ...

messages 設定為依賴項會導致問題。

每次您收到訊息時,setMessages() 會導致组件使用包含已接收訊息的新 messages` 陣列重新渲染。但是,由於此 Effect 現在依賴於 messages,因此這也將重新同步 Effect。 因此,每條新訊息都會使聊天重新連線。使用者不會喜歡這樣!

要解決此問題,請勿在 Effect 內讀取 messages。而是將 更新函式 傳遞給 setMessages

function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
setMessages(msgs => [...msgs, receivedMessage]);
});
return () => connection.disconnect();
}, [roomId]); // ✅ All dependencies declared
// ...

請注意,您的 Effect 現在完全沒有讀取 messages 變數。 您只需要傳遞一個更新函式,例如 msgs => [...msgs, receivedMessage]。React 會 將您的更新函式放入佇列中,並在下一次渲染期間將 msgs 引數提供給它。這就是 Effect 本身不再需要依賴 messages 的原因。由於此修復,接收聊天訊息將不再使聊天重新連線。

您是否想要讀取一個值而不「反應」它的變化?

建構中

本節描述 **尚未在 React 穩定版本中發佈的實驗性 API**。

假設您希望在使用者收到新訊息時播放聲音,除非 isMutedtrue

function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
const [isMuted, setIsMuted] = useState(false);

useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
setMessages(msgs => [...msgs, receivedMessage]);
if (!isMuted) {
playSound();
}
});
// ...

由於你的 Effect 現在程式碼中使用了 isMuted,你必須將它加入 dependencies 中。

function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
const [isMuted, setIsMuted] = useState(false);

useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
setMessages(msgs => [...msgs, receivedMessage]);
if (!isMuted) {
playSound();
}
});
return () => connection.disconnect();
}, [roomId, isMuted]); // ✅ All dependencies declared
// ...

問題是每次 isMuted 改變時(例如,當使用者按下「靜音」切換開關時),Effect 都會重新同步,並重新連線到聊天室。這不是想要的使用者體驗!(在此範例中,即使停用程式碼檢查器也無法解決問題 — 如果你這樣做,isMuted 會「卡住」在舊的值。)

要解決這個問題,你需要將不應該是反應式的邏輯從 Effect 中提取出來。你不希望這個 Effect 對 isMuted 的變化做出「反應」。將這個非反應式的邏輯片段移到 Effect Event 中:

import { useState, useEffect, useEffectEvent } from 'react';

function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
const [isMuted, setIsMuted] = useState(false);

const onMessage = useEffectEvent(receivedMessage => {
setMessages(msgs => [...msgs, receivedMessage]);
if (!isMuted) {
playSound();
}
});

useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
onMessage(receivedMessage);
});
return () => connection.disconnect();
}, [roomId]); // ✅ All dependencies declared
// ...

Effect Event 讓你將 Effect 分成反應式部分(應該對 roomId 等反應值及其變化做出「反應」)和非反應式部分(只讀取其最新值,就像 onMessage 讀取 isMuted 一樣)。現在你在 Effect Event 中讀取 isMuted,它就不需要成為 Effect 的 dependency 了。因此,當你切換「靜音」設定時,聊天室不會重新連線,解決了原本的問題!

包裝來自 props 的事件處理器

當你的元件收到一個事件處理器作為 prop 時,你可能會遇到類似的問題。

function ChatRoom({ roomId, onReceiveMessage }) {
const [messages, setMessages] = useState([]);

useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
onReceiveMessage(receivedMessage);
});
return () => connection.disconnect();
}, [roomId, onReceiveMessage]); // ✅ All dependencies declared
// ...

假設父元件在每次渲染時都傳遞一個*不同的* onReceiveMessage 函式。

<ChatRoom
roomId={roomId}
onReceiveMessage={receivedMessage => {
// ...
}}
/>

由於 onReceiveMessage 是一個 dependency,它會導致 Effect 在每次父元件重新渲染後重新同步。這會使其重新連線到聊天室。要解決此問題,請將呼叫包裝在 Effect Event 中。

function ChatRoom({ roomId, onReceiveMessage }) {
const [messages, setMessages] = useState([]);

const onMessage = useEffectEvent(receivedMessage => {
onReceiveMessage(receivedMessage);
});

useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
onMessage(receivedMessage);
});
return () => connection.disconnect();
}, [roomId]); // ✅ All dependencies declared
// ...

Effect Event 不是反應式的,因此你不需要將它們指定為 dependencies。因此,即使父元件傳遞每次重新渲染都不同的函式,聊天室也不會再重新連線。

區分反應式和非反應式程式碼

在此範例中,你希望每次 roomId 改變時記錄一次訪問。你希望在每次記錄中包含目前的 notificationCount,但你*不*希望 notificationCount 的更改觸發記錄事件。

解決方案仍然是將非反應式程式碼分離到 Effect Event 中。

function Chat({ roomId, notificationCount }) {
const onVisit = useEffectEvent(visitedRoomId => {
logVisit(visitedRoomId, notificationCount);
});

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

你希望你的邏輯對 roomId 做出反應,因此你在 Effect 內讀取 roomId。但是,你不希望 notificationCount 的更改記錄額外的訪問,因此你在 Effect Event 內讀取 notificationCount深入瞭解如何使用 Effect Event 從 Effects 讀取最新的 props 和狀態。

某些反應值是否意外更改?

有時,你*確實*希望你的 Effect 對特定值做出「反應」,但該值的更改頻率比你想要的更頻繁,而且可能沒有反映使用者角度的任何實際更改。例如,假設你在元件主體中建立一個 options 物件,然後從 Effect 內部讀取該物件。

function ChatRoom({ roomId }) {
// ...
const options = {
serverUrl: serverUrl,
roomId: roomId
};

useEffect(() => {
const connection = createConnection(options);
connection.connect();
// ...

這個物件是在元件主體中宣告的,所以它是一個 反應值。 當你在 Effect 內讀取像這樣的反應值時,你會將它宣告為 dependency。這可以確保你的 Effect 對其變化做出「反應」。

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

將它宣告為 dependency 很重要!例如,這可以確保如果 roomId 改變,你的 Effect 將使用新的 options 重新連線到聊天室。但是,上面的程式碼也有一個問題。要查看它,請嘗試在下面的沙盒中輸入文字,並觀察控制台中發生的情況。

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('');

  // Temporarily disable the linter to demonstrate the problem
  // eslint-disable-next-line react-hooks/exhaustive-deps
  const options = {
    serverUrl: serverUrl,
    roomId: roomId
  };

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

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

在上面的沙盒中,輸入只會更新 message 狀態變數。從使用者的角度來看,這不應該影響聊天連線。但是,每次你更新 message 時,你的元件都會重新渲染。當你的元件重新渲染時,其中的程式碼會從頭開始再次執行。

在每次重新渲染 ChatRoom 元件時,都會從頭開始建立一個新的 options 物件。React 認為 options 物件與上次渲染期間建立的 options 物件是*不同的物件*。這就是它重新同步你的 Effect(它取決於 options)的原因,並且聊天室會在你輸入時重新連線。

這個問題只會影響物件和函式。在 JavaScript 中,每個新建立的物件和函式都被視為與所有其他物件和函式不同。它們內部的內容是否相同並不重要!

// During the first render
const options1 = { serverUrl: 'https://127.0.0.1:1234', roomId: 'music' };

// During the next render
const options2 = { serverUrl: 'https://127.0.0.1:1234', roomId: 'music' };

// These are two different objects!
console.log(Object.is(options1, options2)); // false

物件和函式 dependencies 可能會使你的 Effect 比你需要的更頻繁地重新同步。

這就是為什麼,只要有可能,你應該盡量避免將物件和函式作為 Effect 的 dependencies。相反,請嘗試將它們移到元件外部、Effect 內部,或從中提取原始值。

將靜態物件和函式移到元件外部

如果物件不依賴任何 props 和狀態,你可以將該物件移到元件外部。

const options = {
serverUrl: 'https://127.0.0.1:1234',
roomId: 'music'
};

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

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

這樣,你向程式碼檢查器*證明*它不是反應式的。它不會因重新渲染而改變,因此它不需要是 dependency。現在重新渲染 ChatRoom 不會導致你的 Effect 重新同步。

這也適用於函式。

function createOptions() {
return {
serverUrl: 'https://127.0.0.1:1234',
roomId: 'music'
};
}

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

useEffect(() => {
const options = createOptions();
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, []); // ✅ All dependencies declared
// ...

由於 `createOptions` 是在元件外部宣告的,它不是一個響應式 (reactive) 值。這就是為什麼它不需要在 Effect 的相依性中指定,以及為什麼它永遠不會導致 Effect 重新同步。

將動態物件和函式移到 Effect 內部

如果您的物件依賴於一些可能會因重新渲染而改變的響應式值,例如 `roomId` prop,您不能將其拉到元件的*外部*。但是,您可以將其建立移到 Effect 程式碼的*內部*

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]); // ✅ All dependencies declared
// ...

現在 `options` 是在 Effect 內部宣告的,它不再是 Effect 的相依性。相反地,Effect 使用的唯一響應式值是 `roomId`。由於 `roomId` 不是物件或函式,您可以確定它不會*無意中*不同。在 JavaScript 中,數字和字串是透過其內容進行比較的。

// During the first render
const roomId1 = 'music';

// During the next render
const roomId2 = 'music';

// These two strings are the same!
console.log(Object.is(roomId1, roomId2)); // true

由於這個修正,如果您編輯輸入框,聊天將不再重新連線。

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

但是,當您更改 `roomId` 下拉選單時,它*會*重新連線,如您所預期。

這也適用於函式。

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]); // ✅ All dependencies declared
// ...

您可以編寫自己的函式,將程式碼片段組合在 Effect 內部。只要您也在 Effect *內部*宣告它們,它們就不是響應式值,因此它們不需要成為 Effect 的相依性。

從物件讀取原始值

有時,您可能會從 props 接收一個物件。

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

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

這裡的風險是父元件會在渲染期間建立物件。

<ChatRoom
roomId={roomId}
options={{
serverUrl: serverUrl,
roomId: roomId
}}
/>

這會導致您的 Effect 在每次父元件重新渲染時重新連線。要解決此問題,請從 Effect 的*外部*讀取物件中的資訊,並避免使用物件和函式相依性。

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

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

邏輯變得有點重複(您在 Effect 外部從物件讀取一些值,然後在 Effect 內部使用相同的值建立一個物件)。但它非常明確地說明了您的 Effect *實際上*依賴於哪些資訊。如果父元件無意中重新建立了一個物件,聊天將不會重新連線。但是,如果 `options.roomId` 或 `options.serverUrl` 確實不同,聊天將會重新連線。

從函式計算原始值

相同的方法也適用於函式。例如,假設父元件傳遞了一個函式。

<ChatRoom
roomId={roomId}
getOptions={() => {
return {
serverUrl: serverUrl,
roomId: roomId
};
}}
/>

為了避免將其設為相依性(並導致其在重新渲染時重新連線),請在 Effect 外部呼叫它。這會提供 `roomId` 和 `serverUrl` 值,它們不是物件,您可以從 Effect 內部讀取它們。

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

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

這僅適用於純粹函式,因為在渲染期間呼叫它們是安全的。如果您的函式是一個事件處理程式,但您不希望它的更改重新同步您的 Effect,請將其包裝到 Effect 事件中

重點回顧

  • 相依性應始終與程式碼匹配。
  • 當您對相依性不滿意時,您需要編輯的是程式碼。
  • 抑制程式碼檢查器會導致非常令人困惑的錯誤,您應該始終避免這樣做。
  • 要移除相依性,您需要向程式碼檢查器「證明」它沒有必要。
  • 如果某些程式碼應該響應特定互動而執行,請將該程式碼移至事件處理程式。
  • 如果 Effect 的不同部分應該因不同原因重新執行,請將其拆分為多個 Effect。
  • 如果您想根據先前的狀態更新某些狀態,請傳遞一個更新函式。
  • 如果您想讀取最新值而不「響應」它,請從 Effect 中提取 Effect 事件。
  • 在 JavaScript 中,如果物件和函式是在不同時間建立的,則它們被認為是不同的。
  • 盡量避免物件和函式相依性。將它們移到元件外部或 Effect 內部。

挑戰 1 4:
修復重置的間隔

此 Effect 設定了一個每秒跳動一次的間隔。您注意到了一些奇怪的事情:似乎每次跳動時,間隔都會被銷毀並重新建立。修復程式碼,使間隔不會不斷地重新建立。

import { useState, useEffect } from 'react';

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

  useEffect(() => {
    console.log('✅ Creating an interval');
    const id = setInterval(() => {
      console.log('⏰ Interval tick');
      setCount(count + 1);
    }, 1000);
    return () => {
      console.log('❌ Clearing an interval');
      clearInterval(id);
    };
  }, [count]);

  return <h1>Counter: {count}</h1>
}