当你编写 Effect 时,linter 会验证你是否已将 Effect 读取的每个响应式值(如 props 和 state)都包含在 Effect 的依赖项列表中。这可确保你的 Effect 与组件的最新 props 和 state 保持同步。不必要的依赖项可能会导致你的 Effect 运行过于频繁,甚至造成无限循环。请按照本指南查看并移除 Effect 中不必要的依赖项。
你将学习
- 如何修复无限 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
// ...
}
Effect 会“响应”响应式值。 由于 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 也是如此! 幸运的是,您可以在下面尝试一系列常见方法。
深入探讨
抑制 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 和 state,则可以将该对象移到组件外部。
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 Event 中。
总结
- 依赖项应始终与代码匹配。
- 当您对依赖项不满意时,您需要编辑的是代码。
- 禁止 linter 会导致非常令人困惑的错误,您应该始终避免这样做。
- 要删除依赖项,您需要向 linter “证明”它不是必需的。
- 如果某些代码应该响应特定的交互而运行,请将该代码移动到事件处理程序。
- 如果您的 Effect 的不同部分应该因为不同的原因重新运行,请将其拆分为多个 Effect。
- 如果您想根据先前的状态更新某些状态,请传递一个更新器函数。
- 如果您想在不“响应”最新值的情况下读取它,请从您的 Effect 中提取一个 Effect Event。
- 在 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> }