效果的生命周期与组件不同。组件可能会挂载、更新或卸载。效果只能做两件事:开始同步某些东西,以及稍后停止同步这些东西。如果你的效果依赖于随时间变化的属性和状态,这个周期可能会发生多次。React 提供了一个 linter 规则来检查你是否正确指定了效果的依赖。这可以让你的效果与最新的属性和状态保持同步。
🌐 Effects have a different lifecycle from components. Components may mount, update, or unmount. An Effect can only do two things: to start synchronizing something, and later to stop synchronizing it. This cycle can happen multiple times if your Effect depends on props and state that change over time. React provides a linter rule to check that you’ve specified your Effect’s dependencies correctly. This keeps your Effect synchronized to the latest props and state.
你将学习到
- 副作用的生命周期与组件的生命周期有何不同
- 如何孤立地考虑每个单独的副作用
- 你的副作用何时需要重新同步,以及为什么
- 如何确定副作用的依赖
- 值是 React 性的意味着什么
- 空的依赖数组意味着什么
- React 如何使用 linter 验证你的依赖是否正确
- 当你不同意 linter 时该怎么办
副作用的生命周期
🌐 The lifecycle of an Effect
每个 React 组件都经历相同的生命周期:
🌐 Every React component goes through the same lifecycle:
- 当组件被添加到屏幕上时,它会被 挂载。
- 当组件接收到新的 props 或 state 时,它会 更新,通常是为了响应某种交互。
- 当一个组件从屏幕上移除时,它会被 卸载。
这是理解组件的一个好方法,但并不适用于 Effects。 相反,尝试将每个 Effect 独立于组件的生命周期进行思考。Effect 描述了如何将外部系统与当前的 props 和 state 同步。随着代码的变化,同步可能需要更频繁或更少地发生。
为了说明这一点,请考虑将你的组件连接到聊天服务器的副作用:
🌐 To illustrate this point, consider this Effect connecting your component to a chat server:
const serverUrl = 'https://localhost:1234';
function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId]);
// ...
}你的效果的主体指定了如何开始同步:
🌐 Your Effect’s body specifies how to start synchronizing:
// ...
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
// ...你的 Effect 返回的清理函数指定了如何停止同步:
🌐 The cleanup function returned by your Effect specifies how to stop synchronizing:
// ...
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
// ...直观上,你可能认为 React 会在组件挂载时开始同步,在组件卸载时停止同步。然而,事情并非如此简单!有时,在组件保持挂载状态期间,也可能需要多次开始和停止同步。
🌐 Intuitively, you might think that React would start synchronizing when your component mounts and stop synchronizing when your component unmounts. However, this is not the end of the story! Sometimes, it may also be necessary to start and stop synchronizing multiple times while the component remains mounted.
让我们看看为什么这很必要、何时会发生,以及你如何可以控制这种行为。
🌐 Let’s look at why this is necessary, when it happens, and how you can control this behavior.
为什么同步可能需要发生不止一次
🌐 Why synchronization may need to happen more than once
想象一下,这个 ChatRoom 组件接收一个 roomId 属性,该属性是用户在下拉菜单中选择的。假设最初用户选择了 "general" 房间作为 roomId。你的应用显示了 "general" 聊天室:
🌐 Imagine this ChatRoom component receives a roomId prop that the user picks in a dropdown. Let’s say that initially the user picks the "general" room as the roomId. Your app displays the "general" chat room:
const serverUrl = 'https://localhost:1234';
function ChatRoom({ roomId /* "general" */ }) {
// ...
return <h1>Welcome to the {roomId} room!</h1>;
}在 UI 显示之后,React 会运行你的 Effect 来开始同步。 它会连接到 "general" 房间:
🌐 After the UI is displayed, React will run your Effect to start synchronizing. It connects to the "general" room:
function ChatRoom({ roomId /* "general" */ }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // Connects to the "general" room
connection.connect();
return () => {
connection.disconnect(); // Disconnects from the "general" room
};
}, [roomId]);
// ...到目前为止,一切都很好。
🌐 So far, so good.
随后,用户在下拉菜单中选择了另一个房间(例如,"travel")。首先,React 会更新用户界面:
🌐 Later, the user picks a different room in the dropdown (for example, "travel"). First, React will update the UI:
function ChatRoom({ roomId /* "travel" */ }) {
// ...
return <h1>Welcome to the {roomId} room!</h1>;
}考虑接下来应该发生什么。用户看到 "travel" 是界面中选中的聊天房间。然而,上一次运行的 Effect 仍然连接到 "general" 房间。roomId 属性已经改变,所以你当时的 Effect(连接到 "general" 房间)不再与界面匹配。
🌐 Think about what should happen next. The user sees that "travel" is the selected chat room in the UI. However, the Effect that ran the last time is still connected to the "general" room. The roomId prop has changed, so what your Effect did back then (connecting to the "general" room) no longer matches the UI.
此时,你想让 React 做两件事:
🌐 At this point, you want React to do two things:
- 停止与旧的
roomId同步(从"general"房间断开连接) - 开始与新的
roomId同步(连接到"travel"房间)
**幸运的是,你已经教会了 React 如何做这两件事!**你的 Effect 的主体指定了如何开始同步,而你的清理函数指定了如何停止同步。现在 React 所需要做的就是以正确的顺序并使用正确的 props 和 state 来调用它们。让我们看看这到底是如何发生的。
React 如何重新同步你的副作用
🌐 How React re-synchronizes your Effect
请记住,你的 ChatRoom 组件已经收到了其 roomId 属性的新值。它之前是 "general",现在是 "travel"。React 需要重新同步你的 Effect,以便将你重新连接到不同的房间。
🌐 Recall that your ChatRoom component has received a new value for its roomId prop. It used to be "general", and now it is "travel". React needs to re-synchronize your Effect to re-connect you to a different room.
要**停止同步,**React 会在连接到 "general" 房间后调用你的 Effect 返回的清理函数。由于 roomId 是 "general",清理函数会断开与 "general" 房间的连接:
🌐 To stop synchronizing, React will call the cleanup function that your Effect returned after connecting to the "general" room. Since roomId was "general", the cleanup function disconnects from the "general" room:
function ChatRoom({ roomId /* "general" */ }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // Connects to the "general" room
connection.connect();
return () => {
connection.disconnect(); // Disconnects from the "general" room
};
// ...然后 React 将在此次渲染期间运行你提供的 Effect。这一次,roomId 是 "travel",所以它将 开始同步 到 "travel" 聊天室(直到它的清理函数最终被调用):
🌐 Then React will run the Effect that you’ve provided during this render. This time, roomId is "travel" so it will start synchronizing to the "travel" chat room (until its cleanup function is eventually called too):
function ChatRoom({ roomId /* "travel" */ }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // Connects to the "travel" room
connection.connect();
// ...多亏了这个,你现在连接到了用户在界面中选择的同一个房间。灾难避免了!
🌐 Thanks to this, you’re now connected to the same room that the user chose in the UI. Disaster averted!
每次你的组件在 roomId 改变后重新渲染时,你的 Effect 都会重新同步。例如,假设用户将 roomId 从 "travel" 改为 "music"。React 会再次通过调用其清理函数(将你从 "travel" 房间中断开)来停止同步你的 Effect。然后,它会通过使用新的 roomId 属性运行 Effect 的主体(将你连接到 "music" 房间)来开始同步。
🌐 Every time after your component re-renders with a different roomId, your Effect will re-synchronize. For example, let’s say the user changes roomId from "travel" to "music". React will again stop synchronizing your Effect by calling its cleanup function (disconnecting you from the "travel" room). Then it will start synchronizing again by running its body with the new roomId prop (connecting you to the "music" room).
最后,当用户切换到不同的屏幕时,ChatRoom 会被卸载。现在完全不需要保持连接了。React 将最后一次停止同步你的 Effect,并将你从 "music" 聊天室中断开。
🌐 Finally, when the user goes to a different screen, ChatRoom unmounts. Now there is no need to stay connected at all. React will stop synchronizing your Effect one last time and disconnect you from the "music" chat room.
从副作用的角度思考
🌐 Thinking from the Effect’s perspective
让我们从 ChatRoom 组件的角度回顾一下发生的所有事情:
🌐 Let’s recap everything that’s happened from the ChatRoom component’s perspective:
ChatRoom安装了roomId并设置为"general"ChatRoom已更新,roomId设置为"travel"ChatRoom已更新,roomId设置为"music"ChatRoom已卸载
在组件生命周期的每个阶段,你的副作用都会做不同的事情:
🌐 During each of these points in the component’s lifecycle, your Effect did different things:
- 你的效果已连接到
"general"房间 - 你的效果已从
"general"房间断开,并连接到"travel"房间 - 你的效果已从
"travel"房间断开,并连接到"music"房间 - 你的效果已从
"music"房间断开
现在让我们从副作用本身的角度来思考发生了什么:
🌐 Now let’s think about what happened from the perspective of the Effect itself:
useEffect(() => {
// Your Effect connected to the room specified with roomId...
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
// ...until it disconnected
connection.disconnect();
};
}, [roomId]);此代码的结构可能会启发你将所发生的事情视为一系列非重叠时间段:
🌐 This code’s structure might inspire you to see what happened as a sequence of non-overlapping time periods:
- 你的效果已连接到
"general"房间(直到断开连接) - 你的效果已连接到
"travel"房间(直到断开连接) - 你的效果已连接到
"music"房间(直到断开连接)
以前,你是从组件的角度来思考的。当你从组件的角度看时,很容易将 Effects 视为在特定时间触发的“回调”或“生命周期事件”,比如“渲染之后”或“卸载之前”。这种思考方式会很快变得复杂,所以最好避免。
🌐 Previously, you were thinking from the component’s perspective. When you looked from the component’s perspective, it was tempting to think of Effects as “callbacks” or “lifecycle events” that fire at a specific time like “after a render” or “before unmount”. This way of thinking gets complicated very fast, so it’s best to avoid.
相反,总是一次专注于一个启动/停止周期。组件是挂载、更新还是卸载都不应该影响。你只需要描述如何开始同步以及如何停止同步。如果你做得好,你的 Effect 将能够在需要时多次启动和停止而保持稳定。
这可能会让你想起,当你编写创建 JSX 的渲染逻辑时,你并不考虑组件是正在挂载还是更新。你描述屏幕上应该显示的内容,而 React 负责处理其余的部分。
🌐 This might remind you how you don’t think whether a component is mounting or updating when you write the rendering logic that creates JSX. You describe what should be on the screen, and React figures out the rest.
React 如何验证你的副作用可以重新同步
🌐 How React verifies that your Effect can re-synchronize
这里有一个可以互动的实时示例。点击“打开聊天”来挂载 ChatRoom 组件:
🌐 Here is a live example that you can play with. Press “Open chat” to mount the ChatRoom component:
import { useState, useEffect } from 'react'; import { createConnection } from './chat.js'; const serverUrl = 'https://localhost:1234'; function ChatRoom({ roomId }) { useEffect(() => { const connection = createConnection(serverUrl, roomId); connection.connect(); return () => connection.disconnect(); }, [roomId]); return <h1>Welcome to the {roomId} room!</h1>; } export default function App() { const [roomId, setRoomId] = useState('general'); const [show, setShow] = useState(false); return ( <> <label> Choose the chat room:{' '} <select value={roomId} onChange={e => setRoomId(e.target.value)} > <option value="general">general</option> <option value="travel">travel</option> <option value="music">music</option> </select> </label> <button onClick={() => setShow(!show)}> {show ? 'Close chat' : 'Open chat'} </button> {show && <hr />} {show && <ChatRoom roomId={roomId} />} </> ); }
请注意,当组件第一次挂载时,你会看到三个日志:
🌐 Notice that when the component mounts for the first time, you see three logs:
✅ Connecting to "general" room at https://localhost:1234...(仅开发使用)❌ Disconnected from "general" room at https://localhost:1234.(仅开发使用)✅ Connecting to "general" room at https://localhost:1234...
前两个日志仅用于开发。在开发中,React 总是会重新挂载每个组件一次。
🌐 The first two logs are development-only. In development, React always remounts each component once.
React 通过在开发环境中立即强制执行 Effect 来验证它是否可以重新同步。 这可能让你联想到打开门然后再关一次门以检查门锁是否正常工作。React 会在开发环境中额外启动和停止你的 Effect 一次,以检查你是否很好地实现了它的清理。
你的 Effect 在实际中会重新同步的主要原因是它使用的一些数据发生了变化。在上面的沙箱中,改变所选的聊天室。注意,当 roomId 发生变化时,你的 Effect 会重新同步。
🌐 The main reason your Effect will re-synchronize in practice is if some data it uses has changed. In the sandbox above, change the selected chat room. Notice how, when the roomId changes, your Effect re-synchronizes.
然而,也有一些更不寻常的情况需要重新同步。例如,尝试在上面的沙箱中打开聊天时编辑 serverUrl。注意效果如何在你对代码进行编辑时重新同步。将来,React 可能会添加更多依赖重新同步的功能。
🌐 However, there are also more unusual cases in which re-synchronization is necessary. For example, try editing the serverUrl in the sandbox above while the chat is open. Notice how the Effect re-synchronizes in response to your edits to the code. In the future, React may add more features that rely on re-synchronization.
React 如何知道它需要重新同步副作用
🌐 How React knows that it needs to re-synchronize the Effect
你可能会想知道 React 是如何知道你的 Effect 在 roomId 变化后需要重新同步的。这是因为你告诉了 React 其代码依赖于 roomId,通过将其包含在依赖列表:中
🌐 You might be wondering how React knew that your Effect needed to re-synchronize after roomId changes. It’s because you told React that its code depends on roomId by including it in the list of dependencies:
function ChatRoom({ roomId }) { // The roomId prop may change over time
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // This Effect reads roomId
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId]); // So you tell React that this Effect "depends on" roomId
// ...这是它的工作原理:
🌐 Here’s how this works:
- 你知道
roomId是一个属性,这意味着它可以随时间变化。 - 你知道你的效果读取的是
roomId(因此其逻辑依赖于可能会改变的值)。 - 这就是为什么你将其指定为你的 Effect 的依赖(这样当
roomId变化时它会重新同步)。
每次在你的组件重新渲染之后,React 都会查看你传入的依赖数组。如果数组中的任何一个值与上一次渲染时你传入的对应位置的值不同,React 就会重新同步你的 Effect。
🌐 Every time after your component re-renders, React will look at the array of dependencies that you have passed. If any of the values in the array is different from the value at the same spot that you passed during the previous render, React will re-synchronize your Effect.
例如,如果你在初始渲染时传递了 ["general"],然后在下一次渲染时传递了 ["travel"],React 将会比较 "general" 和 "travel"。这些是不同的值(与 Object.is 相比),所以 React 会重新同步你的 Effect。另一方面,如果你的组件重新渲染,但 roomId 没有变化,你的 Effect 将保持连接到同一个房间。
🌐 For example, if you passed ["general"] during the initial render, and later you passed ["travel"] during the next render, React will compare "general" and "travel". These are different values (compared with Object.is), so React will re-synchronize your Effect. On the other hand, if your component re-renders but roomId has not changed, your Effect will remain connected to the same room.
每个副作用代表一个单独的同步过程
🌐 Each Effect represents a separate synchronization process
不要仅仅因为某些逻辑需要与你已经编写的 Effect 同时运行,就将无关的逻辑添加到你的 Effect 中。例如,假设你想在用户访问房间时发送一个分析事件。你已经有一个依赖于 roomId 的 Effect,所以你可能会想把分析调用添加到那里:
🌐 Resist adding unrelated logic to your Effect only because this logic needs to run at the same time as an Effect you already wrote. For example, let’s say you want to send an analytics event when the user visits the room. You already have an Effect that depends on roomId, so you might feel tempted to add the analytics call there:
function ChatRoom({ roomId }) {
useEffect(() => {
logVisit(roomId);
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId]);
// ...
}但想象一下,后来你向这个 Effect 添加了另一个需要重新建立连接的依赖。如果这个 Effect 重新同步,它也会为同一个房间调用 logVisit(roomId),而这是你不打算做的。记录访问 是一个独立的过程,与连接不同。将它们写成两个独立的 Effects:
🌐 But imagine you later add another dependency to this Effect that needs to re-establish the connection. If this Effect re-synchronizes, it will also call logVisit(roomId) for the same room, which you did not intend. Logging the visit is a separate process from connecting. Write them as two separate Effects:
function ChatRoom({ roomId }) {
useEffect(() => {
logVisit(roomId);
}, [roomId]);
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
// ...
}, [roomId]);
// ...
}你代码中的每个 Effect 都应代表一个单独且独立的同步过程。
在上面的例子中,删除一个 Effect 并不会破坏另一个 Effect 的逻辑。这很好地表明它们同步的是不同的内容,因此将它们拆分开是有意义的。另一方面,如果你把一个内聚的逻辑拆分成多个 Effect,代码可能看起来“更干净”,但更难维护。这就是为什么你应该考虑这些过程是相同还是独立,而不是代码看起来是否更干净。
🌐 In the above example, deleting one Effect wouldn’t break the other Effect’s logic. This is a good indication that they synchronize different things, and so it made sense to split them up. On the other hand, if you split up a cohesive piece of logic into separate Effects, the code may look “cleaner” but will be more difficult to maintain. This is why you should think whether the processes are same or separate, not whether the code looks cleaner.
Effects 对响应式值作出“反应”
🌐 Effects “react” to reactive values
你的效果读取了两个变量(serverUrl 和 roomId),但你只指定了 roomId 作为依赖:
🌐 Your Effect reads two variables (serverUrl and roomId), but you only specified roomId as a dependency:
const serverUrl = 'https://localhost:1234';
function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId]);
// ...
}为什么 serverUrl 不需要作为依赖?
🌐 Why doesn’t serverUrl need to be a dependency?
这是因为 serverUrl 从未因重新渲染而改变。无论组件重新渲染多少次,它始终是相同的。这也是为什么 serverUrl 从未改变,因此将其指定为依赖没有意义。毕竟,依赖只有在随时间发生变化时才会起作用!
🌐 This is because the serverUrl never changes due to a re-render. It’s always the same no matter how many times the component re-renders and why. Since serverUrl never changes, it wouldn’t make sense to specify it as a dependency. After all, dependencies only do something when they change over time!
另一方面,roomId 在重新渲染时可能会不同。在组件内部声明的 props、状态和其他值是_响应式的_,因为它们在渲染期间被计算,并参与 React 数据流。
🌐 On the other hand, roomId may be different on a re-render. Props, state, and other values declared inside the component are reactive because they’re calculated during rendering and participate in the React data flow.
如果 serverUrl 是一个状态变量,它将是响应式的。响应式值必须包含在依赖中:
🌐 If serverUrl was a state variable, it would be reactive. Reactive values must be included in dependencies:
function ChatRoom({ roomId }) { // Props change over time
const [serverUrl, setServerUrl] = useState('https://localhost:1234'); // State may change over time
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // Your Effect reads props and state
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId, serverUrl]); // So you tell React that this Effect "depends on" on props and state
// ...
}通过将 serverUrl 包含为依赖,你可以确保在它发生变化后 Effect 会重新同步。
🌐 By including serverUrl as a dependency, you ensure that the Effect re-synchronizes after it changes.
尝试更改所选的聊天室或在此沙盒中编辑服务器 URL:
🌐 Try changing the selected chat room or edit the server URL in this sandbox:
import { useState, useEffect } from 'react'; import { createConnection } from './chat.js'; function ChatRoom({ roomId }) { const [serverUrl, setServerUrl] = useState('https://localhost:1234'); useEffect(() => { const connection = createConnection(serverUrl, roomId); connection.connect(); return () => connection.disconnect(); }, [roomId, serverUrl]); return ( <> <label> Server URL:{' '} <input value={serverUrl} onChange={e => setServerUrl(e.target.value)} /> </label> <h1>Welcome to the {roomId} room!</h1> </> ); } export default function App() { const [roomId, setRoomId] = useState('general'); return ( <> <label> Choose the chat room:{' '} <select value={roomId} onChange={e => setRoomId(e.target.value)} > <option value="general">general</option> <option value="travel">travel</option> <option value="music">music</option> </select> </label> <hr /> <ChatRoom roomId={roomId} /> </> ); }
每当你更改像 roomId 或 serverUrl 这样的响应值时,Effect 会重新连接到聊天服务器。
🌐 Whenever you change a reactive value like roomId or serverUrl, the Effect re-connects to the chat server.
具有空依赖的副作用意味着什么
🌐 What an Effect with empty dependencies means
如果你将 serverUrl 和 roomId 都移到组件外,会发生什么?
🌐 What happens if you move both serverUrl and roomId outside the component?
const serverUrl = 'https://localhost:1234';
const roomId = 'general';
function ChatRoom() {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, []); // ✅ All dependencies declared
// ...
}现在你的 Effect 的代码没有使用 任何 响应式值,所以它的依赖可以是空的([])。
🌐 Now your Effect’s code does not use any reactive values, so its dependencies can be empty ([]).
从组件的角度思考,空的 [] 依赖数组意味着这个 Effect 只在组件挂载时连接到聊天室,并且只在组件卸载时断开连接。(请记住,React 在开发环境中仍然会额外重新同步一次以对你的逻辑进行压力测试。)
🌐 Thinking from the component’s perspective, the empty [] dependency array means this Effect connects to the chat room only when the component mounts, and disconnects only when the component unmounts. (Keep in mind that React would still re-synchronize it an extra time in development to stress-test your logic.)
import { useState, useEffect } from 'react'; import { createConnection } from './chat.js'; const serverUrl = 'https://localhost:1234'; const roomId = 'general'; function ChatRoom() { useEffect(() => { const connection = createConnection(serverUrl, roomId); connection.connect(); return () => connection.disconnect(); }, []); return <h1>Welcome to the {roomId} room!</h1>; } export default function App() { const [show, setShow] = useState(false); return ( <> <button onClick={() => setShow(!show)}> {show ? 'Close chat' : 'Open chat'} </button> {show && <hr />} {show && <ChatRoom />} </> ); }
然而,如果你从 Effect 的角度考虑, 你根本不需要考虑挂载和卸载。重要的是你已经指定了你的 Effect 在开始和停止同步时所做的事情。现在,它没有任何响应式依赖。但如果你希望用户随时间改变 roomId 或 serverUrl(它们将变为响应式),你的 Effect 代码不会改变。你只需要将它们添加到依赖中即可。
🌐 However, if you think from the Effect’s perspective, you don’t need to think about mounting and unmounting at all. What’s important is you’ve specified what your Effect does to start and stop synchronizing. Today, it has no reactive dependencies. But if you ever want the user to change roomId or serverUrl over time (and they would become reactive), your Effect’s code won’t change. You will only need to add them to the dependencies.
组件主体中声明的所有变量都是反应式的
🌐 All variables declared in the component body are reactive
Props 和 state 并不是唯一的响应式值。从它们计算出来的值也是响应式的。如果 props 或 state 发生变化,你的组件会重新渲染,而从它们计算出的值也会随之变化。这就是为什么组件主体中由 Effect 使用的所有变量都应该在 Effect 的依赖列表中。
🌐 Props and state aren’t the only reactive values. Values that you calculate from them are also reactive. If the props or state change, your component will re-render, and the values calculated from them will also change. This is why all variables from the component body used by the Effect should be in the Effect dependency list.
假设用户可以在下拉菜单中选择一个聊天服务器,但他们也可以在设置中配置一个默认服务器。假设你已经将设置状态放在 context 中,因此你可以从该上下文中读取 settings。现在你根据 props 中选择的服务器和默认服务器计算 serverUrl:
🌐 Let’s say that the user can pick a chat server in the dropdown, but they can also configure a default server in settings. Suppose you’ve already put the settings state in a context so you read the settings from that context. Now you calculate the serverUrl based on the selected server from props and the default server:
function ChatRoom({ roomId, selectedServerUrl }) { // roomId is reactive
const settings = useContext(SettingsContext); // settings is reactive
const serverUrl = selectedServerUrl ?? settings.defaultServerUrl; // serverUrl is reactive
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // Your Effect reads roomId and serverUrl
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId, serverUrl]); // So it needs to re-synchronize when either of them changes!
// ...
}在这个例子中,serverUrl 既不是 prop 也不是状态变量。它是一个在渲染期间计算的普通变量。但是它是在渲染期间计算的,因此它可能会因为重新渲染而改变。这就是它具有响应性的原因。
🌐 In this example, serverUrl is not a prop or a state variable. It’s a regular variable that you calculate during rendering. But it’s calculated during rendering, so it can change due to a re-render. This is why it’s reactive.
组件内部的所有值(包括 props、state 以及组件主体中的变量)都是响应式的。任何响应式值在重新渲染时都可能发生变化,因此你需要将响应式值作为 Effect 的依赖。
换句话说,Effects 会对组件主体中的所有值“反应”.
🌐 In other words, Effects “react” to all values from the component body.
深入研究
🌐 Can global or mutable values be dependencies?
可变值(包括全局变量)不是 React 性的。
🌐 Mutable values (including global variables) aren’t reactive.
像 location.pathname 这样的可变值不能作为依赖。 它是可变的,因此可能在完全不经过 React 渲染数据流的情况下随时发生变化。改变它不会触发组件的重新渲染。因此,即使你在依赖中指定了它,React 也不会知道在它变化时重新同步副作用。这也违反了 React 的规则,因为在渲染过程中读取可变数据(即你计算依赖的时刻)会破坏渲染的纯粹性。相反,你应该使用 useSyncExternalStore 来读取并订阅外部的可变值。
像 ref.current 这样的可变值或从中读取的内容也不能作为依赖。 useRef 返回的 ref 对象本身可以作为依赖,但它的 current 属性是故意可变的。它允许你在不触发重新渲染的情况下跟踪某些内容。 但由于更改它不会触发重新渲染,它不是响应式值,React 不会知道在它变化时重新运行你的 Effect。
正如你将在本页下方了解到的,linter 将自动检查这些问题。
🌐 As you’ll learn below on this page, a linter will check for these issues automatically.
React 验证你是否将每个反应值指定为依赖
🌐 React verifies that you specified every reactive value as a dependency
如果你的 linter 已为 React 配置, 它会检查你的 Effect 代码中使用的每个响应式值是否都被声明为其依赖。例如,这是一个 lint 错误,因为 roomId 和 serverUrl 都是响应式的:
🌐 If your linter is configured for React, it will check that every reactive value used by your Effect’s code is declared as its dependency. For example, this is a lint error because both roomId and serverUrl are reactive:
import { useState, useEffect } from 'react'; import { createConnection } from './chat.js'; function ChatRoom({ roomId }) { // roomId is reactive const [serverUrl, setServerUrl] = useState('https://localhost:1234'); // serverUrl is reactive useEffect(() => { const connection = createConnection(serverUrl, roomId); connection.connect(); return () => connection.disconnect(); }, []); // <-- Something's wrong here! return ( <> <label> Server URL:{' '} <input value={serverUrl} onChange={e => setServerUrl(e.target.value)} /> </label> <h1>Welcome to the {roomId} room!</h1> </> ); } export default function App() { const [roomId, setRoomId] = useState('general'); return ( <> <label> Choose the chat room:{' '} <select value={roomId} onChange={e => setRoomId(e.target.value)} > <option value="general">general</option> <option value="travel">travel</option> <option value="music">music</option> </select> </label> <hr /> <ChatRoom roomId={roomId} /> </> ); }
这看起来可能像是一个 React 错误,但实际上 React 正在指出你代码中的一个 bug。roomId 和 serverUrl 都可能随时间变化,但你忘记在它们变化时重新同步你的 Effect。即使用户在 UI 中选择了不同的值,你仍然会保持与初始的 roomId 和 serverUrl 的连接。
🌐 This may look like a React error, but really React is pointing out a bug in your code. Both roomId and serverUrl may change over time, but you’re forgetting to re-synchronize your Effect when they change. You will remain connected to the initial roomId and serverUrl even after the user picks different values in the UI.
要修复该错误,请按照 linter 的建议,将 roomId 和 serverUrl 指定为你的 Effect 的依赖:
🌐 To fix the bug, follow the linter’s suggestion to specify roomId and serverUrl as dependencies of your Effect:
function ChatRoom({ roomId }) { // roomId is reactive
const [serverUrl, setServerUrl] = useState('https://localhost:1234'); // serverUrl is reactive
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [serverUrl, roomId]); // ✅ All dependencies declared
// ...
}在上面的沙箱中尝试此修复。确认 linter 错误已消失,并且在需要时聊天能够重新连接。
🌐 Try this fix in the sandbox above. Verify that the linter error is gone, and the chat re-connects when needed.
不想重新同步时怎么办
🌐 What to do when you don’t want to re-synchronize
在前面的例子中,你通过将 roomId 和 serverUrl 列为依赖来修复了 lint 错误。
🌐 In the previous example, you’ve fixed the lint error by listing roomId and serverUrl as dependencies.
**然而,你可以通过另一种方式向代码检查器“证明”这些值不是响应式值,**即它们不会因为重新渲染而改变。例如,如果 serverUrl 和 roomId 不依赖于渲染并且总是具有相同的值,你可以将它们移到组件之外。现在它们不需要作为依赖:
const serverUrl = 'https://localhost:1234'; // serverUrl is not reactive
const roomId = 'general'; // roomId is not reactive
function ChatRoom() {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, []); // ✅ All dependencies declared
// ...
}你也可以将它们移动到效果内部。它们在渲染期间不会被计算,因此不会是响应式的:
🌐 You can also move them inside the Effect. They aren’t calculated during rendering, so they’re not reactive:
function ChatRoom() {
useEffect(() => {
const serverUrl = 'https://localhost:1234'; // serverUrl is not reactive
const roomId = 'general'; // roomId is not reactive
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, []); // ✅ All dependencies declared
// ...
}Effects 是反应性的代码块。 当你在其中读取的值发生变化时,它们会重新同步。与每次交互只执行一次的事件处理程序不同,Effects 会在需要同步时运行。
你无法“选择”你的依赖。 你的依赖必须包括你在 Effect 中读取的每个 响应式值。代码检查器会强制执行这一点。有时这可能会导致一些问题,例如无限循环以及你的 Effect 过于频繁地重新同步。不要通过抑制代码检查器来解决这些问题!你应该尝试以下方法:
- 检查你的效果是否表示一个独立的同步过程。 如果你的效果没有同步任何东西,它可能是不必要的。 如果它同步多个独立的东西,请将其拆分开来。
- 如果你想在不“响应”它并重新同步 Effect 的情况下读取 props 或 state 的最新值, 你可以将你的 Effect 拆分为一个响应部分(你会保留在 Effect 中)和一个非响应部分(你将其提取到称为 Effect Event 的东西中)。阅读关于将事件与 Effect 分离的内容。
- 避免依赖对象和函数作为依赖。 如果你在渲染过程中创建对象和函数,然后在 Effect 中读取它们,那么它们在每次渲染时都会不同。这会导致你的 Effect 每次都重新同步。阅读更多关于从 Effects 中移除不必要依赖的内容。
回顾
- 组件可以挂载、更新和卸载。
- 每个副作用都有一个独立于周围组件的生命周期。
- 每个效果描述了一个可以启动和停止的独立同步过程。
- 当你编写和阅读副作用时,请从每个单独的副作用的角度(如何开始和停止同步)而不是从组件的角度(它如何挂载、更新或卸载)思考。
- 在组件主体内声明的值是“响应式”的。
- React 值应该重新同步副作用,因为它们会随时间变化。
- linter 验证副作用中使用的所有 React 值是否都指定为依赖。
- 所有被代码审查工具标记的错误都是合法的。总有办法修复代码以不违反规则。
挑战 1 of 5: 修复每次击键时重新连接
🌐 Fix reconnecting on every keystroke
在这个例子中,ChatRoom 组件在挂载时连接到聊天室,在卸载时断开连接,并在选择不同的聊天室时重新连接。这种行为是正确的,所以你需要保持它的正常工作。
🌐 In this example, the ChatRoom component connects to the chat room when the component mounts, disconnects when it unmounts, and reconnects when you select a different chat room. This behavior is correct, so you need to keep it working.
然而,有一个问题。每当你在底部的消息输入框中输入时,ChatRoom 也会重新连接到聊天。(你可以通过清空控制台并在输入框中输入来注意到这一点。)修复这个问题,以便这种情况不再发生。
🌐 However, there is a problem. Whenever you type into the message box input at the bottom, ChatRoom also reconnects to the chat. (You can notice this by clearing the console and typing into the input.) Fix the issue so that this doesn’t happen.
import { useState, useEffect } from 'react'; import { createConnection } from './chat.js'; const serverUrl = 'https://localhost:1234'; function ChatRoom({ roomId }) { const [message, setMessage] = useState(''); useEffect(() => { const connection = createConnection(serverUrl, roomId); connection.connect(); return () => connection.disconnect(); }); return ( <> <h1>Welcome to the {roomId} room!</h1> <input value={message} onChange={e => setMessage(e.target.value)} /> </> ); } export default function App() { const [roomId, setRoomId] = useState('general'); return ( <> <label> Choose the chat room:{' '} <select value={roomId} onChange={e => setRoomId(e.target.value)} > <option value="general">general</option> <option value="travel">travel</option> <option value="music">music</option> </select> </label> <hr /> <ChatRoom roomId={roomId} /> </> ); }