编写 Effect 时,代码检查器会验证你是否已在 Effect 的依赖项列表中包含 Effect 读取的每个响应式值(如 props 和 state)。这确保你的 Effect 与组件的最新 props 和 state 保持同步。不必要的依赖项可能会导致你的 Effect 运行过于频繁,甚至创建无限循环。请遵循本指南来检查和移除 Effect 中不必要的依赖项。
你将学习
- 如何修复无限 Effect 依赖循环
- 当你想移除依赖项时该怎么做
- 如何在不“响应”它的情况下从 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();
// ...
}
然后,如果将 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(); }, []); // <-- 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} /> </> ); }
根据代码检查器的提示填写它们
function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ All dependencies declared
// ...
}
Effects “响应”响应式值。 由于roomId
是一个响应式值(它可能会因重新渲染而改变),代码检查器会验证你是否已将其指定为依赖项。如果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
是一个响应式值,你无法将其从依赖项列表中移除。代码检查器不允许这样做
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'
// ...
}
代码检查器是对的!由于roomId
可能会随着时间而改变,这会在你的代码中引入错误。
要移除依赖项,请向代码检查器“证明”它不需要成为依赖项。 例如,你可以将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 的代码或响应式值的声明方式。
- 然后,你遵循代码检查器的建议并调整依赖项以匹配你已更改的代码。
- 如果你对依赖项列表不满意,则返回第一步(并再次更改代码)。
最后部分很重要。如果要更改依赖项,请先更改周围的代码。您可以将依赖项列表视为Effect 代码中使用的所有反应式值的列表。 您无需选择列表中要包含的内容。该列表描述您的代码。要更改依赖项列表,请更改代码。
这可能感觉像是在解方程。您可能从一个目标开始(例如,删除一个依赖项),并且需要“找到”与该目标匹配的代码。并非每个人都觉得解方程很有趣,编写 Effect 也一样!幸运的是,下面列出了一些您可以尝试的常用方法。
深入探讨
抑制 linter 会导致非常不直观的错误,难以查找和修复。以下是一个示例
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。您已阅读空 ([]
) 依赖项 可以做到这一点,因此您决定忽略 linter,并强制指定[]
作为依赖项。
此计数器应该每秒增加可通过两个按钮配置的数量。但是,由于您向 React “撒谎”说此 Effect 不依赖任何内容,因此 React 将永远使用初始渲染中的onTick
函数。在该渲染期间,count
为0
,increment
为1
。这就是为什么该渲染中的onTick
总是每秒调用setCount(0 + 1)
,您总是看到1
的原因。当此类错误分散在多个组件中时,更难修复。
总有比忽略 linter 更好的解决方案!要修复此代码,您需要将onTick
添加到依赖项列表中。(为了确保仅设置一次间隔,使onTick
成为 Effect 事件。)
我们建议将依赖项 lint 错误视为编译错误。如果您不抑制它,您将永远不会看到这样的错误。此页面的其余部分介绍了针对这种情况和其他情况的替代方法。
删除不必要的依赖项
每次调整 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);
}
// ...
}
现在代码位于事件处理程序中,它不是反应式的——因此它只会在用户提交表单时运行。阅读更多关于在事件处理程序和 Effect 之间进行选择以及如何删除不必要的 Effect。
您的 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
,因此您必须将其添加到依赖项中。
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都会重新同步并重新连接到聊天。这不是理想的用户体验!(在此示例中,即使禁用linter也不起作用——如果您这样做,isMuted
将“卡住”其旧值。)
要解决此问题,您需要将不应该具有反应性的逻辑从Effect中提取出来。您不希望此Effect“响应”isMuted
中的更改。将此非反应性逻辑移动到Effect事件中:
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事件允许您将Effect拆分为反应性部分(应该“响应”反应式值,例如roomId
及其更改)和非反应性部分(仅读取其最新值,例如onMessage
读取isMuted
)。现在您在Effect事件中读取isMuted
,它不需要成为Effect的依赖项。结果,当您打开和关闭“静音”设置时,聊天不会重新连接,从而解决了原始问题!
从 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
是一个依赖项,因此它会在每次父组件重新渲染后导致 Effect 重新同步。这将使其重新连接到聊天。为了解决这个问题,请将调用包装在 Effect 事件中。
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 事件不是反应式的,因此您不需要将其指定为依赖项。结果,即使父组件传递的函数在每次重新渲染时都不同,聊天也不会重新连接。
分离反应式和非反应式代码
在这个示例中,您希望每次 roomId
更改时记录一次访问。您希望在每次日志中包含当前的 notificationCount
,但您不希望 notificationCount
的更改触发日志事件。
解决方案再次是将非反应式代码拆分为 Effect 事件。
function Chat({ roomId, notificationCount }) {
const onVisit = useEffectEvent(visitedRoomId => {
logVisit(visitedRoomId, notificationCount);
});
useEffect(() => {
onVisit(roomId);
}, [roomId]); // ✅ All dependencies declared
// ...
}
您希望您的逻辑对 roomId
具有反应性,因此您在 Effect 内读取 roomId
。但是,您不希望 notificationCount
的更改记录额外的访问,因此您在 Effect 事件中读取 notificationCount
。了解如何使用 Effect 事件从 Effects 读取最新的 props 和状态。
某些反应式值是否无意中更改?
有时,您确实希望您的 Effect “对”某个值做出反应,但该值的更改频率高于您想要的——并且可能无法反映用户视角的任何实际更改。例如,假设您在组件的主体中创建一个 options
对象,然后从 Effect 内部读取该对象。
function ChatRoom({ roomId }) {
// ...
const options = {
serverUrl: serverUrl,
roomId: roomId
};
useEffect(() => {
const connection = createConnection(options);
connection.connect();
// ...
此对象在组件主体中声明,因此它是一个反应式值。当您在 Effect 内部这样读取反应式值时,您将其声明为依赖项。这确保您的 Effect “对”其更改做出反应。
// ...
useEffect(() => {
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [options]); // ✅ All dependencies declared
// ...
将其声明为依赖项非常重要!例如,这确保如果 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
对象和函数依赖项可能会使您的 Effect 比您需要的更频繁地重新同步。
因此,应尽可能避免将对象和函数作为 Effect 的依赖项。相反,尝试将它们移到组件外部、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
// ...
这样,您可以向 linter 证明它不是反应式的。它不会因重新渲染而更改,因此不需要成为依赖项。现在重新渲染 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
在组件外部声明,因此它不是反应式值。这就是为什么它不需要在 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> }