當你撰寫 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 改變時,它*真的不需要*重新執行。
要更改依賴項,請更改程式碼
你可能已經注意到你的工作流程中的一個模式
- 首先,你更改程式碼你的 Effect 或你的反應值的宣告方式。
- 然後,你遵循 linter 並調整依賴項以匹配你已更改的程式碼。
- 如果你對依賴項列表不滿意,你回到第一步(並再次更改程式碼)。
最後一部分很重要。如果你想更改依賴項,請先更改周圍的程式碼。 你可以將依賴項列表視為 Effect 程式碼使用的所有反應值的列表。 你不*選擇*要放在該列表上的內容。該列表*描述*你的程式碼。要更改依賴項列表,請更改程式碼。
這可能感覺像在解方程式。您可能從一個目標開始(例如,移除一個依賴項),然後您需要“找到”與該目標匹配的程式碼。並非每個人都覺得解方程式很有趣,對於撰寫 Effects 來說也是如此!幸運的是,您可以嘗試以下列出的常見方法。
深入探討
抑制程式碼檢查器的警告會導致非常不直觀的錯誤,這些錯誤難以發現和修復。這裡有一個例子
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
函式。在該渲染期間, count
為 0
且 increment
為 1
。這就是為什麼該渲染中的 onTick
總是每秒呼叫 setCount(0 + 1)
,而您總是看到 1
。當這類錯誤分散在多個組件中時,更難以修復。
總有比忽略程式碼檢查器更好的解決方案!要修復此程式碼,您需要將 onTick
新增到依賴項列表中。(為了確保間隔只設定一次,將 onTick
設為 Effect 事件。)
我們建議將依賴項程式碼檢查器錯誤視為編譯錯誤。如果您不抑制它,您將永遠不會看到這類錯誤。 本頁的其餘部分說明了此情況和其他情況的替代方案。
移除不必要的依賴項
每次調整 Effect 的依賴項以反映程式碼時,請查看依賴項列表。當任何這些依賴項發生變化時,Effect 重新執行是否有意義?有時,答案是“否”
- 您可能希望在不同條件下重新執行 Effect 的*不同部分*。
- 您可能只想讀取某些依賴項的*最新值*,而不是“反應”其變化。
- 依賴項可能會*無意中*過於頻繁地更改,因為它是物件或函式。
要找到正確的解決方案,您需要回答一些關於您的 Effect 的問題。讓我們逐步了解它們。
此程式碼是否應該移至事件處理常式?
您應該考慮的第一件事是這段程式碼是否應該是一個 Effect。
想像一個表單。送出時,您將 submitted
狀態變數設定為 true
。您需要傳送 POST 請求並顯示通知。您已將此邏輯放入一個“反應” submitted
為 true
的 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)
。因此,您將會不必要地多次重新擷取城市列表。
這段程式碼的問題在於您正在同步兩個不同的、不相關的事物。
- 您想要根據
國家
屬性將城市
狀態與網路同步。 - 您想要根據
城市
狀態將地區
狀態與網路同步。
將邏輯拆分為兩個 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
的原因。由於此修復,接收聊天訊息將不再使聊天重新連線。
您是否想要讀取一個值而不「反應」它的變化?
假設您希望在使用者收到新訊息時播放聲音,除非 isMuted
為 true
。
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> }