反应式副作用的生命周期

副作用与组件有不同的生命周期。组件可以挂载、更新或卸载。副作用只能做两件事:开始同步某些东西,然后停止同步。如果你的副作用依赖于随时间变化的属性和状态,这个循环可能会发生多次。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.

你将学习到

  • 副作用的生命周期与组件的生命周期有何不同

    ¥How an Effect’s lifecycle is different from a component’s lifecycle

  • 如何孤立地考虑每个单独的副作用

    ¥How to think about each individual Effect in isolation

  • 你的副作用何时需要重新同步,以及为什么

    ¥When your Effect needs to re-synchronize, and why

  • 如何确定副作用的依赖

    ¥How your Effect’s dependencies are determined

  • 值是 React 性的意味着什么

    ¥What it means for a value to be reactive

  • 空的依赖数组意味着什么

    ¥What an empty dependency array means

  • React 如何使用 linter 验证你的依赖是否正确

    ¥How React verifies your dependencies are correct with a linter

  • 当你不同意 linter 时该怎么办

    ¥What to do when you disagree with the linter

副作用的生命周期

¥The lifecycle of an Effect

每个 React 组件都经历相同的生命周期:

¥Every React component goes through the same lifecycle:

  • 当一个组件被添加到屏幕上时它就会被挂载。

    ¥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.

  • 当组件从屏幕上移除时,组件将卸载。

    ¥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.

为了说明这一点,请考虑将你的组件连接到聊天服务器的副作用:

¥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.

注意

有些副作用根本不返回清理函数。通常情况下, 你会想要返回一个 - 但如果你不这样做,React 的行为就像你返回一个空的清理函数一样。

¥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.

为什么同步可能需要发生不止一次

¥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 将运行你的副作用以开始同步。它连接到 "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 将更新 UI:

¥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>;
}

想想接下来会发生什么。用户在 UI 中看到 "travel" 是选定的聊天室。但是,上次运行的副作用仍然连接到 "general" 房间。roomId 属性已更改,因此你的副作用当时所做的(连接到 "general" 房间)不再与 UI 匹配。

¥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:

  1. 停止与旧 roomId 同步(与 "general" 房间断开连接)

    ¥Stop synchronizing with the old roomId (disconnect from the "general" room)

  2. 开始同步新的 roomId(连接到 "travel" 房间)

    ¥Start synchronizing with the new roomId (connect to the "travel" room)

幸运的是,你已经教会了 React 如何做这两件事!你的副作用主体指定如何开始同步,而你的清理函数指定如何停止同步。React 现在需要做的就是以正确的顺序、正确的属性 和状态调用它们。让我们看看究竟是如何发生的。

¥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 如何重新同步你的副作用

¥How React re-synchronizes your Effect

回想一下,你的 ChatRoom 组件已收到其 roomId 属性的新值。以前是 "general",现在是 "travel"。React 需要重新同步你的副作用以将你重新连接到不同的房间。

¥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" 房间后返回的清理函数。由于 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 将运行你在此渲染期间提供的副作用。这次,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();
// ...

因此,你现在已连接到用户在 UI 中选择的同一个房间。灾难化解!

¥Thanks to this, you’re now connected to the same room that the user chose in the UI. Disaster averted!

每次你的组件使用不同的 roomId 重新渲染后,你的副作用将重新同步。例如,假设用户将 roomId"travel" 更改为 "music"。React 将通过调用其清理函数(断开你与 "travel" 房间的连接)再次停止同步你的副作用。然后它将通过使用新的 roomId 属性运行其主体来再次开始同步(将你连接到 "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 将停止最后一次同步你的副作用,并断开你与 "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:

  1. ChatRoom 挂载 roomId 设置为 "general"

    ¥ChatRoom mounted with roomId set to "general"

  2. ChatRoom 已更新,roomId 设置为 "travel"

    ¥ChatRoom updated with roomId set to "travel"

  3. ChatRoom 已更新,roomId 设置为 "music"

    ¥ChatRoom updated with roomId set to "music"

  4. ChatRoom 未挂载

    ¥ChatRoom unmounted

在组件生命周期的每个阶段,你的副作用都会做不同的事情:

¥During each of these points in the component’s lifecycle, your Effect did different things:

  1. 你的副作用连接到 "general" 房间

    ¥Your Effect connected to the "general" room

  2. 你的副作用与 "general" 房间断开连接并连接到 "travel" 房间

    ¥Your Effect disconnected from the "general" room and connected to the "travel" room

  3. 你的副作用与 "travel" 房间断开连接并连接到 "music" 房间

    ¥Your Effect disconnected from the "travel" room and connected to the "music" room

  4. 你的副作用与 "music" 房间断开连接

    ¥Your Effect disconnected from the "music" room

现在让我们从副作用本身的角度来思考发生了什么:

¥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:

  1. 你的副作用器连接到 "general" 房间(直到断开连接)

    ¥Your Effect connected to the "general" room (until it disconnected)

  2. 你的副作用器连接到 "travel" 房间(直到断开连接)

    ¥Your Effect connected to the "travel" room (until it disconnected)

  3. 你的副作用器连接到 "music" 房间(直到断开连接)

    ¥Your Effect connected to the "music" room (until it disconnected)

以前,你是从组件的角度思考的。当你从组件的角度来看时,很容易将副作用视为在特定时间触发的 “callbacks” 或 “生命周期事件”,例如 “渲染之后” 或 “卸载前”。这种思维方式很快就会变得复杂,所以最好避免。

¥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.

相反,始终一次关注一个启动/停止周期。组件是否正在安装、更新或卸载并不重要。你所需要做的就是描述如何开始同步以及如何停止同步。如果做得好,你的效果将能够根据需要多次启动和停止。

¥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.

这可能会提醒你,在编写创建 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:

  1. ✅ Connecting to "general" room at https://localhost:1234...(仅限开发)

    ¥✅ Connecting to "general" room at https://localhost:1234... (development-only)

  2. ❌ Disconnected from "general" room at https://localhost:1234.(仅限开发)

    ¥❌ Disconnected from "general" room at https://localhost:1234. (development-only)

  3. ✅ 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 通过在开发中强制副作用立即重新同步来验证你的副作用是否可以重新同步。这可能会提醒你多花点时间打开和关闭门,以检查门锁是否有效。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.

实际上,你的副作用将重新同步的主要原因是它使用的某些数据是否已更改。在上面的沙盒中,更改所选的聊天室。请注意,当 roomId 更改时,你的副作用如何重新同步。

¥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 如何知道你的副作用在 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:

  1. 你知道 roomId 是属性,这意味着它可以随时间变化。

    ¥You knew roomId is a prop, which means it can change over time.

  2. 你知道你的副作用读取 roomId(因此其逻辑取决于稍后可能更改的值)。

    ¥You knew that your Effect reads roomId (so its logic depends on a value that may change later).

  3. 这就是为什么你将它指定为你的副作用的依赖(以便它在 roomId 更改时重新同步)。

    ¥This is why you specified it as your Effect’s dependency (so that it re-synchronizes when roomId changes).

每次你的组件重新渲染后,React 都会查看你传递的依赖数组。如果数组中的任何值与你在上一次渲染期间传递的同一位置的值不同,React 将重新同步你的副作用。

¥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 将重新同步你的副作用。另一方面,如果你的组件重新渲染但 roomId 没有改变,你的副作用将保持连接到同一个房间。

¥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

不要仅仅因为此逻辑需要与你已编写的副作用同时运行而将不相关的逻辑添加到你的副作用中。例如,假设你要在用户访问房间时发送分析事件。你已经有一个依赖于 roomId 的副作用,因此你可能想在此处添加分析调用:

¥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]);
// ...
}

但是想象一下,你稍后向此副作用添加了另一个需要重新建立连接的依赖。如果这个副作用重新同步,它也会为同一个房间调用 logVisit(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:

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.

在上面的示例中,删除一个副作用不会破坏另一个副作用的逻辑。这很好地表明它们同步不同的事物,因此将它们分开是有意义的。另一方面,如果你将一个内聚的逻辑片段拆分成单独的副作用,代码可能看起来是 “cleaner”,但实际上是 更难维护。。这就是为什么你应该考虑流程是相同的还是分开的,而不是代码是否看起来更清晰。

¥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.

影响 “react” 到无功值

¥Effects “react” to reactive values

你的副作用读取两个变量(serverUrlroomId),但你只将 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 在重新渲染时可能会有所不同。在组件内声明的属性、状态和其他值是响应式的,因为它们是在渲染期间计算并参与 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 是状态变量,它将是 React 性的。React 值必须包含在依赖中:

¥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 作为依赖包含在内,你可以确保副作用在更改后重新同步。

¥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} />
    </>
  );
}

每当你更改 roomIdserverUrl 等 React 值时,副作用器都会重新连接到聊天服务器。

¥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

如果将 serverUrlroomId 都移到组件外部会发生什么情况?

¥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
// ...
}

现在你的副作用代码不使用任何 React 值,因此它的依赖可以为空 ([])。

¥Now your Effect’s code does not use any reactive values, so its dependencies can be empty ([]).

从组件的角度思考,空的 [] 依赖数组意味着这个副作用只有在组件挂载时才连接到聊天室,只有在组件卸载时才断开连接。(请记住,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 />}
    </>
  );
}

但是,如果你是 从副作用的角度思考,,则根本不需要考虑挂载和卸载。重要的是你已经指定了你的副作用将如何开始和停止同步。今天,它没有 React 性依赖。但是,如果你希望用户随着时间的推移更改 roomIdserverUrl(并且它们会变得被动),你的副作用代码将不会更改。你只需要将它们添加到依赖中。

¥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

属性和状态不是唯一的 React 值。你从中计算出的值也是 React 性的。如果属性或状态发生变化,你的组件将重新渲染,并且从中计算出的值也会发生变化。这就是为什么副作用使用的组件主体中的所有变量都应该在副作用依赖列表中的原因。

¥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.

假设用户可以在下拉列表中选择聊天服务器,但他们也可以在设置中配置默认服务器。假设你已经将设置状态放入 上下文,因此你可以从该上下文中读取 settings。现在根据从属性中选择的服务器和默认服务器计算 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 不是属性或状态变量。它是你在渲染期间计算的常规变量。但它是在渲染期间计算的,因此它可能会因重新渲染而发生变化。这就是为什么它是 React 性的。

¥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.

组件内的所有值(包括组件主体中的属性、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.

换句话说,副作用 “react” 到组件主体中的所有值。

¥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 读取和订阅外部可变值

¥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.

ref.current 这样的可变值或你从中读取的内容也不能是依赖。useRef 本身返回的引用对象可以是依赖,但它的 current 属性是有意可变的。它让你 在不触发重新渲染的情况下跟踪某些内容。 但是因为改变它不会触发重新渲染,它不是一个 React 值,并且 React 不会知道在它改变时重新运行你的副作用。

¥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.

正如你将在本页下方了解到的,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 配置,,它将检查你的副作用代码使用的每个反应值是否都声明为它的依赖。例如,这是一个 lint 错误,因为 roomIdserverUrl 都是 React 性的:

¥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 指出了你代码中的错误。roomIdserverUrl 都可能随时间发生变化,但你忘记在它们发生变化时重新同步你的副作用器。即使用户在 UI 中选择了不同的值,你仍将保持与初始 roomIdserverUrl 的连接。

¥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 的建议将 roomIdserverUrl 指定为副作用的依赖:

¥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.

注意

在某些情况下,React 知道一个值永远不会改变,即使它是在组件内部声明的。例如,useState 返回的 set 函数useRef 返回的引用对象是稳定的 - 它们保证在重新渲染时不会改变。稳定值不是 React 性的,因此你可以从列表中省略它们。允许包括它们:它们不会改变,所以没关系。

¥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.

不想重新同步时怎么办

¥What to do when you don’t want to re-synchronize

在前面的示例中,你通过将 roomIdserverUrl 列为依赖来修复了 lint 错误。

¥In the previous example, you’ve fixed the lint error by listing roomId and serverUrl as dependencies.

但是,你可以改为 “prove” 告知 linter,这些值不是反应值,即它们不能因重新渲染而更改。例如,如果 serverUrlroomId 不依赖于渲染并且始终具有相同的值,则可以将它们移到组件之外。现在它们不需要成为依赖:

¥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:

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 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.

你不能 “挑选” 你的依赖。你的依赖必须包括你在副作用中读取的每个 反应值。linter 强制执行此操作。有时这可能会导致无限循环等问题,并导致你的副作用过于频繁地重新同步。不要通过抑制 linter 来解决这些问题!以下是尝试的方法:

¥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:

  • 检查你的副作用是否代表一个独立的同步过程。如果你的副作用不同步任何东西,这可能是不必要的。 如果它同步几个独立的东西,把它分开。

    ¥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.

  • 如果你想读取没有 “reacting” 的属性或状态的最新值并重新同步副作用,你可以将副作用分成反应部分(你将保留在副作用中)和非反应部分(其中 你将提取到称为效果事件的东西)。阅读有关将事件与副作用分开的信息。

    ¥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.

  • 避免依赖对象和函数作为依赖。如果你在渲染期间创建对象和函数,然后从副作用中读取它们,则它们在每次渲染时都会不同。这将导致你的副作用器每次都重新同步。阅读有关从副作用中删除不必要的依赖的更多信息。

    ¥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.

易犯错误

linter 是你的朋友,但它的权力是有限的。linter 只知道依赖何时出错。它不知道解决每种情况的最佳方法。如果 linter 提示存在依赖,但添加它会导致循环,这并不意味着 linter 应该被忽略。你需要更改副作用内部(或外部)的代码,以便该值不是 React 性的并且不需要是依赖。

¥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.

如果你有一个现有的代码库,你可能有一些抑制 linter 的副作用,如下所示:

¥If you have an existing codebase, you might have some Effects that suppress the linter like this:

useEffect(() => {
// ...
// 🔴 Avoid suppressing the linter like this:
// eslint-ignore-next-line react-hooks/exhaustive-deps
}, []);

下一个 pages 上,你将学习如何在不违反规则的情况下修复此代码。它总是值得修复!

¥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.

  • 每个副作用都描述了一个可以启动和停止的单独同步过程。

    ¥Each Effect describes a separate synchronization process that can start and stop.

  • 当你编写和阅读副作用时,请从每个单独的副作用的角度(如何开始和停止同步)而不是从组件的角度(它如何挂载、更新或卸载)思考。

    ¥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).

  • 组件体内声明的值是 “反应式”。

    ¥Values declared inside the component body are “reactive”.

  • React 值应该重新同步副作用,因为它们会随时间变化。

    ¥Reactive values should re-synchronize the Effect because they can change over time.

  • linter 验证副作用中使用的所有 React 值是否都指定为依赖。

    ¥The linter verifies that all reactive values used inside the Effect are specified as dependencies.

  • linter 标记的所有错误都是合法的。总有一种方法可以修复代码以不违反规则。

    ¥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

在此示例中,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} />
    </>
  );
}


React 中文网 - 粤ICP备13048890号