响应式 Effect 的生命周期 | Lifecycle of Reactive Effects

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. Effect 与组件有不同的生命周期。组件可以挂载、更新或卸载。Effect 只能做两件事:开始同步某些东西,然后停止同步它。如果 Effect 依赖于随时间变化的 props 和 state,这个循环可能会发生多次。React 提供了代码检查规则来检查是否正确地指定了 Effect 的依赖项,这能够使 Effect 与最新的 props 和 state 保持同步。

你将会学习到

  • How an Effect’s lifecycle is different from a component’s lifecycle
  • Effect 的生命周期与组件的生命周期有何不同
  • How to think about each individual Effect in isolation
  • 如何独立地考虑每个 Effect
  • When your Effect needs to re-synchronize, and why
  • 什么时候以及为什么 Effect 需要重新同步
  • How your Effect’s dependencies are determined
  • 如何确定 Effect 的依赖项
  • What it means for a value to be reactive
  • 值是响应式的含义是什么
  • What an empty dependency array means
  • 空依赖数组意味着什么
  • How React verifies your dependencies are correct with a linter
  • React 如何使用检查工具验证依赖关系是否正确
  • What to do when you disagree with the linter
  • 与代码检查工具产生分歧时,该如何处理

Effect 的生命周期 | The lifecycle of an Effect

Every React component goes through the same lifecycle: 每个 React 组件都经历相同的生命周期:

  • A component mounts when it’s added to the screen.
  • 当组件被添加到屏幕上时,它会进行组件的 挂载
  • A component updates when it receives new props or state, usually in response to an interaction.
  • 当组件接收到新的 props 或 state 时,通常是作为对交互的响应,它会进行组件的 更新
  • A component unmounts when it’s removed from the screen.
  • 当组件从屏幕上移除时,它会进行组件的 卸载

It’s a good way to think about components, but not about Effects. Instead, try to think about each Effect independently from your component’s lifecycle. An Effect describes how to synchronize an external system to the current props and state. As your code changes, synchronization will need to happen more or less often. 这是一种很好的思考组件的方式,但并不适用于 Effect。相反,尝试从组件生命周期中跳脱出来,独立思考 Effect。Effect 描述了如何将外部系统与当前的 props 和 state 同步。随着代码的变化,同步的频率可能会增加或减少。

To illustrate this point, consider this Effect connecting your component to a chat server: 为了说明这一点,考虑下面这个示例。Effect 将组件连接到聊天服务器:

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: Effect 的主体部分指定了如何 开始同步

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

The cleanup function returned by your Effect specifies how to stop synchronizing: Effect 返回的清理函数指定了如何 停止同步

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

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. 你可能会直观地认为当组件挂载时 React 会 开始同步,而当组件卸载时会 停止同步。然而,事情并没有这么简单!有时,在组件保持挂载状态的同时,可能还需要 多次开始和停止同步

Let’s look at why this is necessary, when it happens, and how you can control this behavior. 让我们来看看 为什么 这是必要的、何时 会发生以及 如何 控制这种行为。

注意

Some Effects don’t return a cleanup function at all. More often than not, you’ll want to return one—but if you don’t, React will behave as if you returned an empty cleanup function. 有些 Effect 根本不返回清理函数。在大多数情况下,可能希望返回一个清理函数,但如果没有返回,React 将表现得好像返回了一个空的清理函数。

为什么同步可能需要多次进行 | Why synchronization may need to happen more than once

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: 想象一下,这个 ChatRoom 组件接收 roomId 属性,用户可以在下拉菜单中选择。假设初始时,用户选择了 "general" 作为 roomId。应用程序会显示 "general" 聊天室:

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

function ChatRoom({ roomId /* "general" */ }) {
// ...
return <h1>欢迎来到 {roomId} 房间!</h1>;
}

After the UI is displayed, React will run your Effect to start synchronizing. It connects to the "general" room: 在 UI 显示之后,React 将运行 Effect 来 开始同步。它连接到 "general" 聊天室:

function ChatRoom({ roomId /* "general" */ }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // 连接到 "general" 聊天室 // Connects to the "general" room
connection.connect();
return () => {
connection.disconnect(); // 断开与 "general" 聊天室的连接 // Disconnects from the "general" room
};
}, [roomId]);
// ...

So far, so good. 到目前为止,一切都很顺利。

Later, the user picks a different room in the dropdown (for example, "travel"). First, React will update the UI: 之后,用户在下拉菜单中选择了不同的房间(例如 "travel" )。首先,React 会更新 UI:

function ChatRoom({ roomId /* "travel" */ }) {
// ...
return <h1>欢迎来到 {roomId} 房间!</h1>;
}

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. 思考接下来应该发生什么。用户在界面中看到 "travel" 是当前选定的聊天室。然而,上次运行的 Effect 仍然连接到 "general" 聊天室。roomId 属性已经发生了变化,所以之前 Effect 所做的事情(连接到 "general" 聊天室)不再与 UI 匹配

At this point, you want React to do two things: 此时,你希望 React 执行两个操作:

  1. Stop synchronizing with the old roomId (disconnect from the "general" room)
  • 停止与旧的 roomId 同步(断开与 "general" 聊天室的连接)
  1. Start synchronizing with the new roomId (connect to the "travel" room)
  • 开始与新的 roomId 同步(连接到 "travel" 聊天室)

Luckily, you’ve already taught React how to do both of these things! Your Effect’s body specifies how to start synchronizing, and your cleanup function specifies how to stop synchronizing. All that React needs to do now is to call them in the correct order and with the correct props and state. Let’s see how exactly that happens. 幸运的是,你已经教会了 React 如何执行这两个操作!Effect 的主体部分指定了如何开始同步,而清理函数指定了如何停止同步。现在,React 只需要按照正确的顺序和正确的 props 和 state 来调用它们。让我们看看具体是如何实现的。

React 如何重新同步 Effect | How React re-synchronizes your 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. 回想一下,ChatRoom 组件已经接收到了 roomId 属性的新值。之前它是 "general",现在变成了 "travel"。React 需要重新同步 Effect,以重新连接到不同的聊天室。

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: 为了 停止同步,React 将调用 Effect 返回的清理函数,该函数在连接到 "general" 聊天室后返回。由于 roomId"general",清理函数将断开与 "general" 聊天室的连接:

function ChatRoom({ roomId /* "general" */ }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // 连接到 "general" 聊天室 // Connects to the "general" room
connection.connect();
return () => {
connection.disconnect(); // 断开与 "general" 聊天室的连接 // Disconnects from the "general" room
};
// ...

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): 然后,React 将运行在此渲染期间提供的 Effect。这次,roomId"travel",因此它将 开始同步"travel" 聊天室(直到最终也调用了清理函数):

function ChatRoom({ roomId /* "travel" */ }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // 连接到 "travel" 聊天室 // 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! 多亏了这一点,现在已经连接到了用户在 UI 中选择的同一个聊天室。避免了灾难!

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). 每当组件使用不同的 roomId 重新渲染后,Effect 将重新进行同步。例如,假设用户将 roomId"travel" 更改为 "music"。React 将再次通过调用清理函数 停止同步 Effect(断开与 "travel" 聊天室的连接)。然后,它将通过使用新的 roomId 属性再次运行 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. 最后,当用户切换到不同的屏幕时,ChatRoom 组件将被卸载。现在没有必要保持连接了。React 将 最后一次停止同步 Effect,并从 "music" 聊天室断开连接。

从 Effect 的角度思考 | Thinking from the Effect’s perspective

Let’s recap everything that’s happened from the ChatRoom component’s perspective: 让我们总结一下从 ChatRoom 组件的角度所发生的一切:

  1. ChatRoom mounted with roomId set to "general"
  • ChatRoom 组件挂载,roomId 设置为 "general"
  1. ChatRoom updated with roomId set to "travel"
  • ChatRoom 组件更新,roomId 设置为 "travel"
  1. ChatRoom updated with roomId set to "music"
  • ChatRoom 组件更新,roomId 设置为 "music"
  1. ChatRoom unmounted
  • ChatRoom 组件卸载

During each of these points in the component’s lifecycle, your Effect did different things: 在组件生命周期的每个阶段,Effect 执行了不同的操作:

  1. Your Effect connected to the "general" room
  • Effect 连接到了 "general" 聊天室
  1. Your Effect disconnected from the "general" room and connected to the "travel" room
  • Effect 断开了与 "general" 聊天室的连接,并连接到了 "travel" 聊天室
  1. Your Effect disconnected from the "travel" room and connected to the "music" room
  • Effect 断开了与 "travel" 聊天室的连接,并连接到了 "music" 聊天室
  1. Your Effect disconnected from the "music" room
  • Effect 断开了与 "music" 聊天室的连接

Now let’s think about what happened from the perspective of the Effect itself: 现在让我们从 Effect 本身的角度来思考所发生的事情:

useEffect(() => {
// Your Effect connected to the room specified with roomId...
// Effect 连接到了通过 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: 这段代码的结构可能会将所发生的事情看作是一系列不重叠的时间段:

  1. Your Effect connected to the "general" room (until it disconnected)
  • Effect 连接到了 "general" 聊天室(直到断开连接)
  1. Your Effect connected to the "travel" room (until it disconnected)
  • Effect 连接到了 "travel" 聊天室(直到断开连接)
  1. Your Effect connected to the "music" room (until it disconnected)
  • Effect 连接到了 "music" 聊天室(直到断开连接)

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 视为在特定时间点触发的“回调函数”或“生命周期事件”,例如“渲染后”或“卸载前”。这种思维方式很快变得复杂,所以最好避免使用。

Instead, always focus on a single start/stop cycle at a time. It shouldn’t matter whether a component is mounting, updating, or unmounting. All you need to do is to describe how to start synchronization and how to stop it. If you do it well, your Effect will be resilient to being started and stopped as many times as it’s needed. 相反,始终专注于单个启动/停止周期。无论组件是挂载、更新还是卸载,都不应该有影响。只需要描述如何开始同步和如何停止。如果做得好,Effect 将能够在需要时始终具备启动和停止的弹性

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. 这可能会让你想起当编写创建 JSX 的渲染逻辑时,并不考虑组件是挂载还是更新。描述的是应该显示在屏幕上的内容,而 React 会 解决其余的问题

React 如何验证 Effect 可以重新进行同步 | How React verifies that your Effect can re-synchronize

Here is a live example that you can play with. Press “Open chat” to mount the ChatRoom component: 这里有一个可以互动的实时示例。点击“打开聊天”来挂载 ChatRoom 组件:

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>欢迎来到 {roomId} 房间!</h1>;
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  const [show, setShow] = useState(false);
  return (
    <>
      <label>
        选择聊天室:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">所有</option>
          <option value="travel">旅游</option>
          <option value="music">音乐</option>
        </select>
      </label>
      <button onClick={() => setShow(!show)}>
        {show ? '关闭聊天' : '打开聊天'}
      </button>
      {show && <hr />}
      {show && <ChatRoom roomId={roomId} />}
    </>
  );
}

Notice that when the component mounts for the first time, you see three logs: 请注意,当组件首次挂载时,会看到三个日志:

  1. ✅ Connecting to "general" room at https://localhost:1234... (development-only)
  • ✅ 连接到 "general" 聊天室,位于 https://localhost:1234... (仅限开发环境)
  1. ❌ Disconnected from "general" room at https://localhost:1234. (development-only)
  • ❌ 从 "general" 聊天室断开连接,位于 https://localhost:1234. (仅限开发环境)
  1. ✅ Connecting to "general" room at https://localhost:1234...
  • ✅ 连接到 "general" 聊天室,位于 https://localhost:1234...

The first two logs are development-only. In development, React always remounts each component once. 前两个日志仅适用于开发环境。在开发环境中,React 总是会重新挂载每个组件一次。

React verifies that your Effect can re-synchronize by forcing it to do that immediately in development. This might remind you of opening a door and closing it an extra time to check if the door lock works. React starts and stops your Effect one extra time in development to check you’ve implemented its cleanup well. React 通过在开发环境中立即强制 Effect 重新进行同步来验证其是否能够重新同步。这可能让你想起打开门并额外关闭它以检查门锁是否有效的情景。React 在开发环境中额外启动和停止 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. 实际上,Effect 重新进行同步的主要原因是它所使用的某些数据发生了变化。在上面的示例中,更改所选的聊天室。注意当 roomId 发生变化时,Effect 会重新进行同步。

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. 然而,还存在其他一些不寻常的情况需要重新进行同步。例如,在上面的示例中,尝试在聊天打开时编辑 serverUrl。注意当修改代码时,Effect会重新进行同步。将来,React 可能会添加更多依赖于重新同步的功能。

React 如何知道需要重新进行 Effect 的同步 | How React knows that it needs to re-synchronize the Effect

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: 你可能想知道 React 是如何知道在 roomId 更改后需要重新同步 Effect。这是因为 你告诉了 React 它的代码依赖于 roomId,通过将其包含在 依赖列表 中。

function ChatRoom({ roomId }) { // roomId 属性可能会随时间变化。 // The roomId prop may change over time
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // 这个 Effect 读取了 roomId // This Effect reads roomId
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId]); // 因此,你告诉 React 这个 Effect "依赖于" roomId // So you tell React that this Effect "depends on" roomId
// ...

Here’s how this works: 下面是它的工作原理:

  1. You knew roomId is a prop, which means it can change over time.
  • 你知道 roomId 是 prop,这意味着它可能会随着时间的推移发生变化。
  1. You knew that your Effect reads roomId (so its logic depends on a value that may change later).
  • 你知道 Effect 读取了 roomId(因此其逻辑依赖于可能会在之后发生变化的值)。
  1. This is why you specified it as your Effect’s dependency (so that it re-synchronizes when roomId changes).
  • 这就是为什么你将其指定为 Effect 的依赖项(以便在 roomId 发生变化时重新进行同步)。

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. 每次在组件重新渲染后,React 都会查看传递的依赖项数组。如果数组中的任何值与上一次渲染时在相同位置传递的值不同,React 将重新同步 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. 例如,如果在初始渲染时传递了 ["general"],然后在下一次渲染时传递了 ["travel"],React 将比较 "general""travel"。这些是不同的值(使用 Object.is 进行比较),因此 React 将重新同步 Effect。另一方面,如果组件重新渲染但 roomId 没有发生变化,Effect 将继续连接到相同的房间。

每个 Effect 表示一个独立的同步过程。| Each Effect represents a separate synchronization process

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: 抵制将与 Effect 无关的逻辑添加到已经编写的 Effect 中,仅仅因为这些逻辑需要与 Effect 同时运行。例如,假设你想在用户访问房间时发送一个分析事件。你已经有一个依赖于 roomId 的 Effect,所以你可能会想要将分析调用添加到那里:

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

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: 但是想象一下,如果以后给这个 Effect 添加了另一个需要重新建立连接的依赖项。如果这个 Effect 重新进行同步,它将为相同的房间调用 logVisit(roomId),而这不是你的意图。记录访问行为是 一个独立的过程,与连接不同。将它们作为两个单独的 Effect 编写:

function ChatRoom({ roomId }) {
useEffect(() => {
logVisit(roomId);
}, [roomId]);

useEffect(() => {
const connection = createConnection(serverUrl, roomId);
// ...
}, [roomId]);
// ...
}

Each Effect in your code should represent a separate and independent synchronization process. 代码中的每个 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. 在上面的示例中,删除一个 Effect 不会影响另一个 Effect 的逻辑。这表明它们同步不同的内容,因此将它们拆分开是有意义的。另一方面,如果将一个内聚的逻辑拆分成多个独立的 Effects,代码可能会看起来更加“清晰”,但 维护起来会更加困难。这就是为什么你应该考虑这些过程是相同还是独立的,而不是只考虑代码是否看起来更整洁。

Effect 会“响应”于响应式值 | Effects “react” to reactive values

Your Effect reads two variables (serverUrl and roomId), but you only specified roomId as a dependency: Effect 读取了两个变量(serverUrlroomId),但是只将 roomId 指定为依赖项:

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

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

Why doesn’t serverUrl need to be a dependency? 为什么 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! 这是因为 serverUrl 永远不会因为重新渲染而发生变化。无论组件重新渲染多少次以及原因是什么,serverUrl 都保持不变。既然 serverUrl 从不变化,将其指定为依赖项就没有意义。毕竟,依赖项只有在随时间变化时才会起作用!

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. 另一方面,roomId 在重新渲染时可能会不同。在组件内部声明的 props、state 和其他值都是 响应式 的,因为它们是在渲染过程中计算的,并参与了 React 的数据流

If serverUrl was a state variable, it would be reactive. Reactive values must be included in dependencies: 如果 serverUrl 是状态变量,那么它就是响应式的。响应式值必须包含在依赖项中:

function ChatRoom({ roomId }) { // Props 随时间变化 // Props change over time
const [serverUrl, setServerUrl] = useState('https://localhost:1234'); // State 可能随时间变化 // State may change over time

useEffect(() => {
const connection = createConnection(serverUrl, roomId); // Effect 读取 props 和 state // Your Effect reads props and state
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId, serverUrl]); // 因此,你告诉 React 这个 Effect "依赖于" props 和 state // So you tell React that this Effect "depends on" on props and state
// ...
}

By including serverUrl as a dependency, you ensure that the Effect re-synchronizes after it changes. 通过将 serverUrl 包含在依赖项中,确保 Effect 在其发生变化后重新同步。

Try changing the selected chat room or edit the server URL in this sandbox: 尝试在此沙盒中更改所选的聊天室或编辑服务器 URL:

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>
        服务器 URL:{' '}
        <input
          value={serverUrl}
          onChange={e => setServerUrl(e.target.value)}
        />
      </label>
      <h1>欢迎来到 {roomId} 房间!</h1>
    </>
  );
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  return (
    <>
      <label>
        选择聊天室:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">所有</option>
          <option value="travel">旅游</option>
          <option value="music">音乐</option>
        </select>
      </label>
      <hr />
      <ChatRoom roomId={roomId} />
    </>
  );
}

Whenever you change a reactive value like roomId or serverUrl, the Effect re-connects to the chat server. 无论何时更改一个类似 roomIdserverUrl 的响应式值,该 Effect 都会重新连接到聊天服务器。

没有依赖项的 Effect 的含义 | What an Effect with empty dependencies means

What happens if you move both serverUrl and roomId outside the component? 如果将 serverUrlroomId 都移出组件会发生什么?

const serverUrl = 'https://localhost:1234';
const roomId = 'general';

function ChatRoom() {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, []); // ✅ 声明的所有依赖 // ✅ All dependencies declared
// ...
}

Now your Effect’s code does not use any reactive values, so its dependencies can be empty ([]). 现在 Effect 的代码不使用任何响应式值,因此它的依赖可以是空的 ([])。

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.) 从组件的角度来看,空的 [] 依赖数组意味着这个 Effect 仅在组件挂载时连接到聊天室,并在组件卸载时断开连接。(请记住,在开发环境中,React 仍会 额外执行一次 来对逻辑进行压力测试。)

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>欢迎来到 {roomId} 房间!</h1>;
}

export default function App() {
  const [show, setShow] = useState(false);
  return (
    <>
      <button onClick={() => setShow(!show)}>
        {show ? '关闭聊天' : '打开聊天'}
      </button>
      {show && <hr />}
      {show && <ChatRoom />}
    </>
  );
}

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. 然而,如果你 从 Effect 的角度思考,根本不需要考虑挂载和卸载。重要的是,你已经指定了 Effect 如何开始和停止同步。目前,它没有任何响应式依赖。但是,如果希望用户随时间改变 roomIdserverUrl(它们将变为响应式),Effect 的代码不需要改变。只需要将它们添加到依赖项中即可。

在组件主体中声明的所有变量都是响应式的 | All variables declared in the component body are reactive

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. Props 和 state 并不是唯一的响应式值。从它们计算出的值也是响应式的。如果 props 或 state 发生变化,组件将重新渲染,从中计算出的值也会随之改变。这就是为什么 Effect 使用的组件主体中的所有变量都应该在依赖列表中。

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: 假设用户可以在下拉菜单中选择聊天服务器,但他们还可以在设置中配置默认服务器。假设你已经将设置状态放入了 上下文,因此从该上下文中读取 settings。现在,可以根据 props 中选择的服务器和默认服务器来计算 serverUrl

function ChatRoom({ roomId, selectedServerUrl }) { // roomId 是响应式的 // roomId is reactive
const settings = useContext(SettingsContext); // settings 是响应式的 // settings is reactive
const serverUrl = selectedServerUrl ?? settings.defaultServerUrl; // serverUrl 是响应式的 // serverUrl is reactive
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // Effect 读取了 roomId 和 serverUrl // Your Effect reads roomId and serverUrl
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId, serverUrl]); // 因此,当它们中的任何一个发生变化时,它需要重新同步! // So it needs to re-synchronize when either of them changes!
// ...
}

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. 在这个例子中,serverUrl 不是 prop 或 state 变量。它是在渲染过程中计算的普通变量。但是它是在渲染过程中计算的,所以它可能会因为重新渲染而改变。这就是为什么它是响应式的。

All values inside the component (including props, state, and variables in your component’s body) are reactive. Any reactive value can change on a re-render, so you need to include reactive values as Effect’s dependencies. 组件内部的所有值(包括 props、state 和组件体内的变量)都是响应式的。任何响应式值都可以在重新渲染时发生变化,所以需要将响应式值包括在 Effect 的依赖项中

In other words, Effects “react” to all values from the component body. 换句话说,Effect 对组件体内的所有值都会“react”。

深入探讨

全局变量或可变值可以作为依赖项吗?| Can global or mutable values be dependencies?

Mutable values (including global variables) aren’t reactive. 可变值(包括全局变量)不是响应式的。

A mutable value like location.pathname can’t be a dependency. It’s mutable, so it can change at any time completely outside of the React rendering data flow. Changing it wouldn’t trigger a re-render of your component. Therefore, even if you specified it in the dependencies, React wouldn’t know to re-synchronize the Effect when it changes. This also breaks the rules of React because reading mutable data during rendering (which is when you calculate the dependencies) breaks purity of rendering. Instead, you should read and subscribe to an external mutable value with useSyncExternalStore. 例如,像 location.pathname 这样的可变值不能作为依赖项。它是可变的,因此可以在 React 渲染数据流之外的任何时间发生变化。更改它不会触发组件的重新渲染。因此,即使在依赖项中指定了它,React 也无法知道在其更改时重新同步 Effect。这也违反了 React 的规则,因为在渲染过程中读取可变数据(即在计算依赖项时)会破坏 纯粹的渲染。相反,应该使用 useSyncExternalStore 来读取和订阅外部可变值。

A mutable value like ref.current or things you read from it also can’t be a dependency. The ref object returned by useRef itself can be a dependency, but its current property is intentionally mutable. It lets you keep track of something without triggering a re-render. But since changing it doesn’t trigger a re-render, it’s not a reactive value, and React won’t know to re-run your Effect when it changes. 另外,像 ref.current 或从中读取的值也不能作为依赖项。useRef 返回的 ref 对象本身可以作为依赖项,但其 current 属性是有意可变的。它允许 跟踪某些值而不触发重新渲染。但由于更改它不会触发重新渲染,它不是响应式值,React 不会知道在其更改时重新运行 Effect。

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

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: 如果检查工具 配置了 React,它将检查 Effect 代码中使用的每个响应式值是否已声明为其依赖项。例如,以下示例是一个 lint 错误,因为 roomIdserverUrl 都是响应式的:

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

function ChatRoom({ roomId }) { // roomId 是响应式的 // roomId is reactive
  const [serverUrl, setServerUrl] = useState('https://localhost:1234'); // serverUrl 是响应式的 // serverUrl is reactive

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => connection.disconnect();
  }, []); // <-- 这里有些问题! // <-- Something's wrong here!

  return (
    <>
      <label>
        服务器 URL:{' '}
        <input
          value={serverUrl}
          onChange={e => setServerUrl(e.target.value)}
        />
      </label>
      <h1>欢迎来到 {roomId} 房间!</h1>
    </>
  );
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  return (
    <>
      <label>
        选择聊天室:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">所有</option>
          <option value="travel">旅游</option>
          <option value="music">音乐</option>
        </select>
      </label>
      <hr />
      <ChatRoom roomId={roomId} />
    </>
  );
}

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. 这可能看起来像是 React 错误,但实际上 React 是在指出代码中的 bug。roomIdserverUrl 都可能随时间改变,但忘记了在它们改变时重新同步 Effect。即使用户在 UI 中选择了不同的值,仍然保持连接到初始的 roomIdserverUrl

To fix the bug, follow the linter’s suggestion to specify roomId and serverUrl as dependencies of your Effect: 要修复这个 bug,请按照检查工具的建议将 roomIdserverUrl 作为 Effect 的依赖进行指定:

function ChatRoom({ roomId }) { // roomId 是响应式的 // roomId is reactive
const [serverUrl, setServerUrl] = useState('https://localhost:1234'); // serverUrl 是响应式的 // serverUrl is reactive
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [serverUrl, roomId]); // ✅ 声明的所有依赖 // ✅ All dependencies declared
// ...
}

Try this fix in the sandbox above. Verify that the linter error is gone, and the chat re-connects when needed. 在上面的沙盒中尝试这个修复方法。验证一下是否消除了检查工具的错误,并且在需要时聊天会重新连接。

注意

In some cases, React knows that a value never changes even though it’s declared inside the component. For example, the set function returned from useState and the ref object returned by useRef are stable—they are guaranteed to not change on a re-render. Stable values aren’t reactive, so you may omit them from the list. Including them is allowed: they won’t change, so it doesn’t matter. 在某些情况下,React 知道 一个值永远不会改变,即使它在组件内部声明。例如,从 useState 返回的 set 函数和从 useRef 返回的 ref 对象是 稳定的 ——它们保证在重新渲染时不会改变。稳定值不是响应式的,因此可以从列表中省略它们。包括它们是允许的:它们不会改变,所以无关紧要。

当你不想进行重新同步时该怎么办 | What to do when you don’t want to re-synchronize

In the previous example, you’ve fixed the lint error by listing roomId and serverUrl as dependencies. 在上一个示例中,通过将 roomIdserverUrl 列为依赖项来修复了 lint 错误。

However, you could instead “prove” to the linter that these values aren’t reactive values, i.e. that they can’t change as a result of a re-render. For example, if serverUrl and roomId don’t depend on rendering and always have the same values, you can move them outside the component. Now they don’t need to be dependencies: 然而,可以通过向检查工具“证明”这些值不是响应式值,即它们 不会 因为重新渲染而改变。例如,如果 serverUrlroomId 不依赖于渲染并且始终具有相同的值,可以将它们移到组件外部。现在它们不需要成为依赖项:

const serverUrl = 'https://localhost:1234'; // serverUrl 不是响应式的 // serverUrl is not reactive
const roomId = 'general'; // roomId 不是响应式的 // 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: 也可以将它们 移动到 Effect 内部。它们不是在渲染过程中计算的,因此它们不是响应式的:

function ChatRoom() {
useEffect(() => {
const serverUrl = 'https://localhost:1234'; // serverUrl 不是响应式的 // serverUrl is not reactive
const roomId = 'general'; // roomId 不是响应式的 // roomId is not reactive
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, []); // ✅ 声明的所有依赖 // ✅ All dependencies declared
// ...
}

Effects are reactive blocks of code. They re-synchronize when the values you read inside of them change. Unlike event handlers, which only run once per interaction, Effects run whenever synchronization is necessary. Effect 是一段响应式的代码块。它们在读取的值发生变化时重新进行同步。与事件处理程序不同,事件处理程序只在每次交互时运行一次,而 Effect 则在需要进行同步时运行。

You can’t “choose” your dependencies. Your dependencies must include every reactive value you read in the Effect. The linter enforces this. Sometimes this may lead to problems like infinite loops and to your Effect re-synchronizing too often. Don’t fix these problems by suppressing the linter! Here’s what to try instead: 不能“选择”依赖项。依赖项必须包括 Effect 中读取的每个 响应式值。代码检查工具会强制执行此规则。有时,这可能会导致出现无限循环的问题,或者 Effect 过于频繁地重新进行同步。不要通过禁用代码检查来解决这些问题!下面是一些解决方案:

  • Check that your Effect represents an independent synchronization process. If your Effect doesn’t synchronize anything, it might be unnecessary. If it synchronizes several independent things, split it up.

  • 检查 Effect 是否表示了独立的同步过程。如果 Effect 没有进行任何同步操作,可能是不必要的。如果它同时进行了几个独立的同步操作,将其拆分为多个 Effect

  • If you want to read the latest value of props or state without “reacting” to it and re-synchronizing the Effect, you can split your Effect into a reactive part (which you’ll keep in the Effect) and a non-reactive part (which you’ll extract into something called an Effect Event). Read about separating Events from Effects.

  • 如果想读取 props 或 state 的最新值,但又不想对其做出反应并重新同步 Effect,可以将 Effect 拆分为具有反应性的部分(保留在 Effect 中)和非反应性的部分(提取为名为 “Effect Event” 的内容)。阅读关于将事件与 Effect 分离的内容

  • Avoid relying on objects and functions as dependencies. If you create objects and functions during rendering and then read them from an Effect, they will be different on every render. This will cause your Effect to re-synchronize every time. Read more about removing unnecessary dependencies from Effects.

  • 避免将对象和函数作为依赖项。如果在渲染过程中创建对象和函数,然后在 Effect 中读取它们,它们将在每次渲染时都不同。这将导致 Effect 每次都重新同步。阅读有关从 Effect 中删除不必要依赖项的更多内容

陷阱

The linter is your friend, but its powers are limited. The linter only knows when the dependencies are wrong. It doesn’t know the best way to solve each case. If the linter suggests a dependency, but adding it causes a loop, it doesn’t mean the linter should be ignored. You need to change the code inside (or outside) the Effect so that that value isn’t reactive and doesn’t need to be a dependency. 检查工具是你的朋友,但它们的能力是有限的。检查工具只知道依赖关系是否 错误。它并不知道每种情况下的 最佳 解决方法。如果静态代码分析工具建议添加某个依赖关系,但添加该依赖关系会导致循环,这并不意味着应该忽略静态代码分析工具。需要修改 Effect 内部(或外部)的代码,使得该值不是响应式的,也不 需要 成为依赖项。

If you have an existing codebase, you might have some Effects that suppress the linter like this: 如果有一个现有的代码库,可能会有一些像这样禁用了检查工具的 Effect:

useEffect(() => {
// ...
// 🔴 Avoid suppressing the linter like this:
// eslint-ignore-next-line react-hooks/exhaustive-deps
// 🔴 避免这样禁用静态代码分析工具:
// eslint-ignore-next-line react-hooks/exhaustive-deps
}, []);

On the next pages, you’ll learn how to fix this code without breaking the rules. It’s always worth fixing! 在 下一页之后的页面 中,你将学习如何修复这段代码,而不违反规则。修复代码总是值得的!

摘要

  • Components can mount, update, and unmount.
  • 组件可以挂载、更新和卸载。
  • Each Effect has a separate lifecycle from the surrounding component.
  • 每个 Effect 与周围组件有着独立的生命周期。
  • Each Effect describes a separate synchronization process that can start and stop.
  • 每个 Effect 描述了一个独立的同步过程,可以 开始停止
  • When you write and read Effects, think from each individual Effect’s perspective (how to start and stop synchronization) rather than from the component’s perspective (how it mounts, updates, or unmounts).
  • 在编写和读取 Effect 时,要独立地考虑每个 Effect(如何开始和停止同步),而不是从组件的角度思考(如何挂载、更新或卸载)。
  • Values declared inside the component body are “reactive”.
  • 在组件主体内声明的值是“响应式”的。
  • Reactive values should re-synchronize the Effect because they can change over time.
  • 响应式值应该重新进行同步 Effect,因为它们可以随着时间的推移而发生变化。
  • The linter verifies that all reactive values used inside the Effect are specified as dependencies.
  • 检查工具验证在 Effect 内部使用的所有响应式值都被指定为依赖项。
  • All errors flagged by the linter are legitimate. There’s always a way to fix the code to not break the rules.
  • 检查工具标记的所有错误都是合理的。总是有一种方法可以修复代码,同时不违反规则。

1挑战 5 个挑战:
修复每次输入均重新连接 | Fix reconnecting on every keystroke

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. 然而,存在一个问题。每当在底部的消息框中输入时,ChatRoom 也会重新连接到聊天室(可以通过清空控制台并在输入框中输入内容来注意到这一点)。修复这个问题,使其不再发生重新连接的情况。

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>欢迎来到 {roomId} 聊天室!</h1>
      <input
        value={message}
        onChange={e => setMessage(e.target.value)}
      />
    </>
  );
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  return (
    <>
      <label>
        选择聊天室:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">所有</option>
          <option value="travel">旅游</option>
          <option value="music">音乐</option>
        </select>
      </label>
      <hr />
      <ChatRoom roomId={roomId} />
    </>
  );
}