移除副作用依赖

当你编写副作用时,linter 将验证你是否已将副作用读取的每个反应值(如属性和状态)包含在副作用的依赖列表中。这确保你的副作用与组件的最新属性和状态保持同步。不必要的依赖可能会导致你的副作用运行过于频繁,甚至会造成无限循环。按照本指南检查并从你的副作用中删除不必要的依赖。

¥When you write an Effect, the linter will verify that you’ve included every reactive value (like props and state) that the Effect reads in the list of your Effect’s dependencies. This ensures that your Effect remains synchronized with the latest props and state of your component. Unnecessary dependencies may cause your Effect to run too often, or even create an infinite loop. Follow this guide to review and remove unnecessary dependencies from your Effects.

你将学习到

  • 如何修复无限副作用依赖循环

    ¥How to fix infinite Effect dependency loops

  • 当你想删除依赖时该怎么做

    ¥What to do when you want to remove a dependency

  • 如何在没有 “reacting” 的情况下从副作用中读取值

    ¥How to read a value from your Effect without “reacting” to it

  • 如何以及为什么要避免对象和函数依赖

    ¥How and why to avoid object and function dependencies

  • 为什么抑制依赖 linter 是危险的,应该怎么做

    ¥Why suppressing the dependency linter is dangerous, and what to do instead

依赖应与代码匹配

¥Dependencies should match the code

当你编写副作用时,你首先指定如何 开始和停止 无论你希望副作用做什么:

¥When you write an Effect, you first specify how to start and stop whatever you want your Effect to be doing:

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

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

然后,如果你将副作用依赖留空 ([]),linter 将建议正确的依赖:

¥Then, if you leave the Effect dependencies empty ([]), the linter will suggest the correct dependencies:

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();
  }, []); // <-- Fix the mistake here!
  return <h1>Welcome to the {roomId} room!</h1>;
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  return (
    <>
      <label>
        Choose the chat room:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">general</option>
          <option value="travel">travel</option>
          <option value="music">music</option>
        </select>
      </label>
      <hr />
      <ChatRoom roomId={roomId} />
    </>
  );
}

根据 linter 所说的填写它们:

¥Fill them in according to what the linter says:

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

影响 “react” 到无功值。 由于 roomId 是一个 React 值(它可能会因重新渲染而改变),linter 会验证你是否已将其指定为依赖。如果 roomId 接收到不同的值,React 将重新同步你的副作用。这确保聊天保持连接到所选房间,“reacts” 连接到下拉列表:

¥Effects “react” to reactive values. Since roomId is a reactive value (it can change due to a re-render), the linter verifies that you’ve specified it as a dependency. If roomId receives a different value, React will re-synchronize your Effect. This ensures that the chat stays connected to the selected room and “reacts” to the dropdown:

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

要删除依赖,请证明它不是依赖

¥To remove a dependency, prove that it’s not a dependency

请注意,你不能对副作用的依赖进行 “挑选”。你的副作用代码使用的每个 reactive value 都必须在你的依赖列表中声明。依赖列表由周围的代码决定:

¥Notice that you can’t “choose” the dependencies of your Effect. Every reactive value used by your Effect’s code must be declared in your dependency list. The dependency list is determined by the surrounding code:

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

function ChatRoom({ roomId }) { // This is a reactive value
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // This Effect reads that reactive value
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ So you must specify that reactive value as a dependency of your Effect
// ...
}

反应的值 包括属性以及直接在组件内部声明的所有变量和函数。由于 roomId 是一个 React 值,你不能将其从依赖列表中删除。linter 不允许这样做:

¥Reactive values include props and all variables and functions declared directly inside of your component. Since roomId is a reactive value, you can’t remove it from the dependency list. The linter wouldn’t allow it:

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

function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, []); // 🔴 React Hook useEffect has a missing dependency: 'roomId'
// ...
}

代码检查器是对的!由于 roomId 可能会随时间变化,这会在你的代码中引入错误。

¥And the linter would be right! Since roomId may change over time, this would introduce a bug in your code.

要删除依赖,“prove” 告诉 linter 它不需要成为依赖。例如,你可以将 roomId 移出你的组件以证明它不是响应式的并且不会在重新渲染时发生变化:

¥To remove a dependency, “prove” to the linter that it doesn’t need to be a dependency. For example, you can move roomId out of your component to prove that it’s not reactive and won’t change on re-renders:

const serverUrl = 'https://localhost:1234';
const roomId = 'music'; // Not a reactive value anymore

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

现在 roomId 不是一个 React 值(并且不能在重新渲染时改变),它不需要是一个依赖:

¥Now that roomId is not a reactive value (and can’t change on a re-render), it doesn’t need to be a dependency:

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

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

export default function ChatRoom() {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => connection.disconnect();
  }, []);
  return <h1>Welcome to the {roomId} room!</h1>;
}

这就是为什么你现在可以指定 空 ([]) 依赖列表。。你的效果实际上不再依赖于任何反应值,因此当任何组件的属性或状态发生更改时,它实际上不需要重新运行。

¥This is why you could now specify an empty ([]) dependency list. Your Effect really doesn’t depend on any reactive value anymore, so it really doesn’t need to re-run when any of the component’s props or state change.

要更改依赖,请更改代码

¥To change the dependencies, change the code

你可能已经注意到工作流程中的一种模式:

¥You might have noticed a pattern in your workflow:

  1. 首先,你更改副作用的代码或如何声明反应值。

    ¥First, you change the code of your Effect or how your reactive values are declared.

  2. 然后,你按照 linter 并调整依赖以匹配你更改的代码。

    ¥Then, you follow the linter and adjust the dependencies to match the code you have changed.

  3. 如果你对依赖列表不满意,可以返回第一步(并再次更改代码)。

    ¥If you’re not happy with the list of dependencies, you go back to the first step (and change the code again).

最后一部分很重要。如果要更改依赖,请先更改周围的代码。你可以将依赖列表视为 你的副作用代码使用的所有 React 值的列表。 你不必选择将什么放在该列表中。该列表描述了你的代码。要更改依赖列表,请更改代码。

¥The last part is important. If you want to change the dependencies, change the surrounding code first. You can think of the dependency list as a list of all the reactive values used by your Effect’s code. You don’t choose what to put on that list. The list describes your code. To change the dependency list, change the code.

这可能感觉像是在求解方程式。你可能从一个目标开始(例如,删除依赖),并且你需要 “寻找” 匹配该目标的代码。不是每个人都觉得解方程式很有趣,写副作用也是一样!幸运的是,下面列出了你可以尝试的常见教程。

¥This might feel like solving an equation. You might start with a goal (for example, to remove a dependency), and you need to “find” the code matching that goal. Not everyone finds solving equations fun, and the same thing could be said about writing Effects! Luckily, there is a list of common recipes that you can try below.

易犯错误

如果你有一个现有的代码库,你可能有一些抑制 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
}, []);

当依赖与代码不匹配时,引入错误的风险非常高。通过抑制 linter,你可以对副作用所依赖的值做出反应。

¥When dependencies don’t match the code, there is a very high risk of introducing bugs. By suppressing the linter, you “lie” to React about the values your Effect depends on.

而是,请使用以下技术。

¥Instead, use the techniques below.

深入研究

为什么抑制依赖 linter 如此危险?

¥Why is suppressing the dependency linter so dangerous?

抑制 linter 会导致非常不直观的错误,这些错误很难找到和修复。这是一个例子:

¥Suppressing the linter leads to very unintuitive bugs that are hard to find and fix. Here’s one example:

import { useState, useEffect } from 'react';

export default function Timer() {
  const [count, setCount] = useState(0);
  const [increment, setIncrement] = useState(1);

  function onTick() {
	setCount(count + increment);
  }

  useEffect(() => {
    const id = setInterval(onTick, 1000);
    return () => clearInterval(id);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return (
    <>
      <h1>
        Counter: {count}
        <button onClick={() => setCount(0)}>Reset</button>
      </h1>
      <hr />
      <p>
        Every second, increment by:
        <button disabled={increment === 0} onClick={() => {
          setIncrement(i => i - 1);
        }}></button>
        <b>{increment}</b>
        <button onClick={() => {
          setIncrement(i => i + 1);
        }}>+</button>
      </p>
    </>
  );
}

假设你想运行副作用 “仅在挂载时”。你已经了解到 空 ([]) 依赖 会这样做,因此你决定忽略 linter,并强制指定 [] 作为依赖。

¥Let’s say that you wanted to run the Effect “only on mount”. You’ve read that empty ([]) dependencies do that, so you’ve decided to ignore the linter, and forcefully specified [] as the dependencies.

该计数器应该每秒递增两个按钮可配置的数量。但是,由于你向 React 表明此副作用不依赖于任何东西,因此 React 将永远使用初始渲染中的 onTick 函数。在那个渲染过程中, count0increment1。这就是为什么渲染中的 onTick 总是每秒调用 setCount(0 + 1),而你总是看到 1。当它们分布在多个组件中时,这样的错误更难修复。

¥This counter was supposed to increment every second by the amount configurable with the two buttons. However, since you “lied” to React that this Effect doesn’t depend on anything, React forever keeps using the onTick function from the initial render. During that render, count was 0 and increment was 1. This is why onTick from that render always calls setCount(0 + 1) every second, and you always see 1. Bugs like this are harder to fix when they’re spread across multiple components.

总有比忽略 linter 更好的解决方案!要修复此代码,你需要将 onTick 添加到依赖列表中。(为确保间隔只设置一次,使 onTick 成为副作用事件。

¥There’s always a better solution than ignoring the linter! To fix this code, you need to add onTick to the dependency list. (To ensure the interval is only setup once, make onTick an Effect Event.)

我们建议将依赖 lint 错误视为编译错误。如果你不抑制它,你永远不会看到这样的错误。本页的其余部分记录了这种情况和其他情况的备选方案。

¥We recommend treating the dependency lint error as a compilation error. If you don’t suppress it, you will never see bugs like this. The rest of this page documents the alternatives for this and other cases.

删除不必要的依赖

¥Removing unnecessary dependencies

每次调整副作用的依赖以反映代码时,请查看依赖列表。当这些依赖中的任何一个发生变化时,副作用重新运行是否有意义?有时,答案是 “no”:

¥Every time you adjust the Effect’s dependencies to reflect the code, look at the dependency list. Does it make sense for the Effect to re-run when any of these dependencies change? Sometimes, the answer is “no”:

  • 你可能希望在不同条件下重新执行副作用器的不同部分。

    ¥You might want to re-execute different parts of your Effect under different conditions.

  • 你可能只想读取某些依赖的最新值而不是 “reacting” 以了解其更改。

    ¥You might want to only read the latest value of some dependency instead of “reacting” to its changes.

  • 依赖可能会无意中频繁更改,因为它是对象或函数。

    ¥A dependency may change too often unintentionally because it’s an object or a function.

要找到正确的解决方案,你需要回答几个关于你的副作用器的问题。让我们来看看它们。

¥To find the right solution, you’ll need to answer a few questions about your Effect. Let’s walk through them.

此代码是否应移至事件处理程序?

¥Should this code move to an event handler?

你首先应该考虑的是这段代码是否应该是一个副作用。

¥The first thing you should think about is whether this code should be an Effect at all.

想象一个表格。提交时,你将 submitted 状态变量设置为 true。你需要发送 POST 请求并显示通知。你已将此逻辑放入 “reacts” 到 submittedtrue 的副作用中:

¥Imagine a form. On submit, you set the submitted state variable to true. You need to send a POST request and show a notification. You’ve put this logic inside an Effect that “reacts” to submitted being true:

function Form() {
const [submitted, setSubmitted] = useState(false);

useEffect(() => {
if (submitted) {
// 🔴 Avoid: Event-specific logic inside an Effect
post('/api/register');
showNotification('Successfully registered!');
}
}, [submitted]);

function handleSubmit() {
setSubmitted(true);
}

// ...
}

后来,你想根据当前主题来设计通知消息的样式,所以你阅读了当前主题。由于 theme 是在组件主体中声明的,它是一个 React 值,因此你将其添加为依赖:

¥Later, you want to style the notification message according to the current theme, so you read the current theme. Since theme is declared in the component body, it is a reactive value, so you add it as a dependency:

function Form() {
const [submitted, setSubmitted] = useState(false);
const theme = useContext(ThemeContext);

useEffect(() => {
if (submitted) {
// 🔴 Avoid: Event-specific logic inside an Effect
post('/api/register');
showNotification('Successfully registered!', theme);
}
}, [submitted, theme]); // ✅ All dependencies declared

function handleSubmit() {
setSubmitted(true);
}

// ...
}

通过这样做,你引入了一个错误。想象一下,你先提交表单,然后在深色和浅色主题之间切换。theme 会改变,副作用会重新运行,所以它会再次显示相同的通知!

¥By doing this, you’ve introduced a bug. Imagine you submit the form first and then switch between Dark and Light themes. The theme will change, the Effect will re-run, and so it will display the same notification again!

这里的问题是,这首先不应该是副作用。你想要发送此 POST 请求并显示通知以响应提交表单,这是一个特定的交互。要运行一些代码以响应特定的交互,请将该逻辑直接放入相应的事件处理程序中:

¥The problem here is that this shouldn’t be an Effect in the first place. You want to send this POST request and show the notification in response to submitting the form, which is a particular interaction. To run some code in response to particular interaction, put that logic directly into the corresponding event handler:

function Form() {
const theme = useContext(ThemeContext);

function handleSubmit() {
// ✅ Good: Event-specific logic is called from event handlers
post('/api/register');
showNotification('Successfully registered!', theme);
}

// ...
}

现在代码在事件处理程序中,它不是 React 性的 - 所以它只会在用户提交表单时运行。阅读有关 在事件处理程序和副作用之间进行选择如何删除不需要的副作用。 的更多信息。

¥Now that the code is in an event handler, it’s not reactive—so it will only run when the user submits the form. Read more about choosing between event handlers and Effects and how to delete unnecessary Effects.

你的副作用是否在做几件不相关的事情?

¥Is your Effect doing several unrelated things?

你应该问自己的下一个问题是你的副作用是否在做几件不相关的事情。

¥The next question you should ask yourself is whether your Effect is doing several unrelated things.

假设你正在创建一个运输表格,用户需要在其中选择他们的城市和地区。你根据选择的 country 从服务器获取 cities 的列表,以在下拉列表中显示它们:

¥Imagine you’re creating a shipping form where the user needs to choose their city and area. You fetch the list of cities from the server according to the selected country to show them in a dropdown:

function ShippingForm({ country }) {
const [cities, setCities] = useState(null);
const [city, setCity] = useState(null);

useEffect(() => {
let ignore = false;
fetch(`/api/cities?country=${country}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setCities(json);
}
});
return () => {
ignore = true;
};
}, [country]); // ✅ All dependencies declared

// ...

This is a good example of 在副作用中获取数据。 你正在根据 country 属性将 cities 状态与网络同步。你不能在事件处理程序中执行此操作,因为你需要在显示 ShippingFormcountry 更改时立即获取(无论是哪种交互导致它)。

¥This is a good example of fetching data in an Effect. You are synchronizing the cities state with the network according to the country prop. You can’t do this in an event handler because you need to fetch as soon as ShippingForm is displayed and whenever the country changes (no matter which interaction causes it).

现在假设你要为城市地区添加第二个选择框,它应该为当前选定的 city 获取 areas。你可以首先为同一副作用中的区域列表添加第二个 fetch 调用:

¥Now let’s say you’re adding a second select box for city areas, which should fetch the areas for the currently selected city. You might start by adding a second fetch call for the list of areas inside the same Effect:

function ShippingForm({ country }) {
const [cities, setCities] = useState(null);
const [city, setCity] = useState(null);
const [areas, setAreas] = useState(null);

useEffect(() => {
let ignore = false;
fetch(`/api/cities?country=${country}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setCities(json);
}
});
// 🔴 Avoid: A single Effect synchronizes two independent processes
if (city) {
fetch(`/api/areas?city=${city}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setAreas(json);
}
});
}
return () => {
ignore = true;
};
}, [country, city]); // ✅ All dependencies declared

// ...

但是,由于副作用现在使用 city 状态变量,你必须将 city 添加到依赖列表中。这反过来又引入了一个问题:当用户选择不同的城市时,副作用将重新运行并调用 fetchCities(country)。结果,你将不必要地多次重新获取城市列表。

¥However, since the Effect now uses the city state variable, you’ve had to add city to the list of dependencies. That, in turn, introduced a problem: when the user selects a different city, the Effect will re-run and call fetchCities(country). As a result, you will be unnecessarily refetching the list of cities many times.

此代码的问题在于你正在同步两个不同的不相关事物:

¥The problem with this code is that you’re synchronizing two different unrelated things:

  1. 你想根据 country 属性将 cities 状态同步到网络。

    ¥You want to synchronize the cities state to the network based on the country prop.

  2. 你想根据 city 状态将 areas 状态同步到网络。

    ¥You want to synchronize the areas state to the network based on the city state.

将逻辑拆分为两个副作用,每个副作用都对需要同步的属性做出 React:

¥Split the logic into two Effects, each of which reacts to the prop that it needs to synchronize with:

function ShippingForm({ country }) {
const [cities, setCities] = useState(null);
useEffect(() => {
let ignore = false;
fetch(`/api/cities?country=${country}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setCities(json);
}
});
return () => {
ignore = true;
};
}, [country]); // ✅ All dependencies declared

const [city, setCity] = useState(null);
const [areas, setAreas] = useState(null);
useEffect(() => {
if (city) {
let ignore = false;
fetch(`/api/areas?city=${city}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setAreas(json);
}
});
return () => {
ignore = true;
};
}
}, [city]); // ✅ All dependencies declared

// ...

现在第一个副作用仅在 country 更改时重新运行,而第二个副作用在 city 更改时重新运行。你已按目的将它们分开:两个不同的事物由两个独立的副作用器同步。两个独立的副作用有两个独立的依赖列表,因此它们不会无意中相互触发。

¥Now the first Effect only re-runs if the country changes, while the second Effect re-runs when the city changes. You’ve separated them by purpose: two different things are synchronized by two separate Effects. Two separate Effects have two separate dependency lists, so they won’t trigger each other unintentionally.

最终的代码比原来的要长一些,但是拆分这些副作用还是正确的。每个副作用应该代表一个独立的同步过程。 在此示例中,删除一个副作用不会破坏另一个副作用的逻辑。这意味着它们同步不同的东西,最好将它们分开。如果你担心重复,可以通过 将重复逻辑提取到自定义钩子中。 改进此代码

¥The final code is longer than the original, but splitting these Effects is still correct. Each Effect should represent an independent synchronization process. In this example, deleting one Effect doesn’t break the other Effect’s logic. This means they synchronize different things, and it’s good to split them up. If you’re concerned about duplication, you can improve this code by extracting repetitive logic into a custom Hook.

你在读一些状态来计算下一个状态吗?

¥Are you reading some state to calculate the next state?

每次新消息到达时,此副作用都会使用新创建的数组更新 messages 状态变量:

¥This Effect updates the messages state variable with a newly created array every time a new message arrives:

function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
setMessages([...messages, receivedMessage]);
});
// ...

它使用从所有现有消息开始的 messages创建一个新数组 变量,并在末尾添加新消息。但是,由于 messages 是副作用读取的 React 值,因此它必须是依赖:

¥It uses the messages variable to create a new array starting with all the existing messages and adds the new message at the end. However, since messages is a reactive value read by an Effect, it must be a dependency:

function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
setMessages([...messages, receivedMessage]);
});
return () => connection.disconnect();
}, [roomId, messages]); // ✅ All dependencies declared
// ...

使 messages 成为依赖会引入一个问题。

¥And making messages a dependency introduces a problem.

每次你收到消息时,setMessages() 都会使组件使用包含收到消息的新 messages 数组重新渲染。但是,由于此副作用现在依赖于 messages,因此这也会重新同步副作用。所以每条新消息都会使聊天重新连接。用户不会喜欢的!

¥Every time you receive a message, setMessages() causes the component to re-render with a new messages array that includes the received message. However, since this Effect now depends on messages, this will also re-synchronize the Effect. So every new message will make the chat re-connect. The user would not like that!

要解决此问题,请不要读取副作用中的 messages。而是,将 更新函数 传递给 setMessages

¥To fix the issue, don’t read messages inside the Effect. Instead, pass an updater function to setMessages:

function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
setMessages(msgs => [...msgs, receivedMessage]);
});
return () => connection.disconnect();
}, [roomId]); // ✅ All dependencies declared
// ...

请注意你的副作用现在根本不读取 messages 变量。你只需要传递一个像 msgs => [...msgs, receivedMessage] 这样的更新函数。React 将你的更新函数放入队列中 并将在下一次渲染期间向其提供 msgs 参数。这就是副作用本身不再需要依赖 messages 的原因。由于此修复,接收聊天消息将不再使聊天重新连接。

¥Notice how your Effect does not read the messages variable at all now. You only need to pass an updater function like msgs => [...msgs, receivedMessage]. React puts your updater function in a queue and will provide the msgs argument to it during the next render. This is why the Effect itself doesn’t need to depend on messages anymore. As a result of this fix, receiving a chat message will no longer make the chat re-connect.

你想读取一个没有 “reacting” 的值来改变它吗?

¥Do you want to read a value without “reacting” to its changes?

开发中

本节介绍尚未在 React 稳定版本中发布的实验性 API。

¥This section describes an experimental API that has not yet been released in a stable version of React.

假设你想在用户收到新消息时播放声音,除非 isMutedtrue

¥Suppose that you want to play a sound when the user receives a new message unless isMuted is true:

function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
const [isMuted, setIsMuted] = useState(false);

useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
setMessages(msgs => [...msgs, receivedMessage]);
if (!isMuted) {
playSound();
}
});
// ...

由于你的副作用现在在其代码中使用 isMuted,因此你必须将其添加到依赖中:

¥Since your Effect now uses isMuted in its code, you have to add it to the dependencies:

function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
const [isMuted, setIsMuted] = useState(false);

useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
setMessages(msgs => [...msgs, receivedMessage]);
if (!isMuted) {
playSound();
}
});
return () => connection.disconnect();
}, [roomId, isMuted]); // ✅ All dependencies declared
// ...

问题是每次 isMuted 改变时(例如,当用户按下 “静音” 开关时),副作用将重新同步,并重新连接到聊天。这不是理想的用户体验!(在这个例子中,即使禁用 linter 也不起作用 - 如果你这样做,isMuted 将得到 “卡住” 的旧值。)

¥The problem is that every time isMuted changes (for example, when the user presses the “Muted” toggle), the Effect will re-synchronize, and reconnect to the chat. This is not the desired user experience! (In this example, even disabling the linter would not work—if you do that, isMuted would get “stuck” with its old value.)

要解决这个问题,需要将不应该响应式的逻辑从副作用中抽取出来。你不想让这个副作用到 “react” 到 isMuted 的变化。将这段非 React 性逻辑移到副作用事件中:

¥To solve this problem, you need to extract the logic that shouldn’t be reactive out of the Effect. You don’t want this Effect to “react” to the changes in isMuted. Move this non-reactive piece of logic into an Effect Event:

import { useState, useEffect, useEffectEvent } from 'react';

function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
const [isMuted, setIsMuted] = useState(false);

const onMessage = useEffectEvent(receivedMessage => {
setMessages(msgs => [...msgs, receivedMessage]);
if (!isMuted) {
playSound();
}
});

useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
onMessage(receivedMessage);
});
return () => connection.disconnect();
}, [roomId]); // ✅ All dependencies declared
// ...

副作用事件让你可以将副作用分成 React 部分(应该是 “react” 到 React 值,比如 roomId 及其变化)和非 React 部分(只读取它们的最新值,比如 onMessage 读取 isMuted)。现在你在副作用事件中读取了 isMuted,它不需要是你的副作用的依赖。因此,当你打开和关闭 “静音” 设置时,聊天不会重新连接,解决了原始问题!

¥Effect Events let you split an Effect into reactive parts (which should “react” to reactive values like roomId and their changes) and non-reactive parts (which only read their latest values, like onMessage reads isMuted). Now that you read isMuted inside an Effect Event, it doesn’t need to be a dependency of your Effect. As a result, the chat won’t re-connect when you toggle the “Muted” setting on and off, solving the original issue!

从属性封装事件处理程序

¥Wrapping an event handler from the props

当你的组件接收事件处理程序作为属性时,你可能会遇到类似的问题:

¥You might run into a similar problem when your component receives an event handler as a prop:

function ChatRoom({ roomId, onReceiveMessage }) {
const [messages, setMessages] = useState([]);

useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
onReceiveMessage(receivedMessage);
});
return () => connection.disconnect();
}, [roomId, onReceiveMessage]); // ✅ All dependencies declared
// ...

假设父组件在每次渲染时传递不同的 onReceiveMessage 函数:

¥Suppose that the parent component passes a different onReceiveMessage function on every render:

<ChatRoom
roomId={roomId}
onReceiveMessage={receivedMessage => {
// ...
}}
/>

由于 onReceiveMessage 是一个依赖,它会导致副作用在每次父级重新渲染后重新同步。这将使它重新连接到聊天。要解决此问题,请将调用封装在副作用事件中:

¥Since onReceiveMessage is a dependency, it would cause the Effect to re-synchronize after every parent re-render. This would make it re-connect to the chat. To solve this, wrap the call in an Effect Event:

function ChatRoom({ roomId, onReceiveMessage }) {
const [messages, setMessages] = useState([]);

const onMessage = useEffectEvent(receivedMessage => {
onReceiveMessage(receivedMessage);
});

useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
onMessage(receivedMessage);
});
return () => connection.disconnect();
}, [roomId]); // ✅ All dependencies declared
// ...

副作用事件不是 React 性的,因此你不需要将它们指定为依赖。因此,即使父组件传递的函数在每次重新渲染时都不同,聊天也将不再重新连接。

¥Effect Events aren’t reactive, so you don’t need to specify them as dependencies. As a result, the chat will no longer re-connect even if the parent component passes a function that’s different on every re-render.

分离反应性和非反应性代码

¥Separating reactive and non-reactive code

在此示例中,你希望在每次 roomId 更改时记录一次访问。你希望在每个日志中包含当前的 notificationCount,但不希望更改 notificationCount 来触发日志事件。

¥In this example, you want to log a visit every time roomId changes. You want to include the current notificationCount with every log, but you don’t want a change to notificationCount to trigger a log event.

解决方案还是将非 React 性代码拆分成一个副作用事件:

¥The solution is again to split out the non-reactive code into an Effect Event:

function Chat({ roomId, notificationCount }) {
const onVisit = useEffectEvent(visitedRoomId => {
logVisit(visitedRoomId, notificationCount);
});

useEffect(() => {
onVisit(roomId);
}, [roomId]); // ✅ All dependencies declared
// ...
}

你希望你的逻辑对 roomId 具有 React 性,因此你在副作用中读取 roomId。但是,你不希望通过更改 notificationCount 来记录额外的访问,因此你在副作用事件中读取 notificationCount详细了解如何使用副作用事件从副作用读取最新的属性和状态。

¥You want your logic to be reactive with regards to roomId, so you read roomId inside of your Effect. However, you don’t want a change to notificationCount to log an extra visit, so you read notificationCount inside of the Effect Event. Learn more about reading the latest props and state from Effects using Effect Events.

一些无功值是否会无意中改变?

¥Does some reactive value change unintentionally?

有时,你确实希望你的副作用 to “react” 达到某个值,但该值的变化比你希望的更频繁 - 并且可能不会从用户的角度反映任何实际变化。例如,假设你在组件主体中创建了一个 options 对象,然后从副作用内部读取该对象:

¥Sometimes, you do want your Effect to “react” to a certain value, but that value changes more often than you’d like—and might not reflect any actual change from the user’s perspective. For example, let’s say that you create an options object in the body of your component, and then read that object from inside of your Effect:

function ChatRoom({ roomId }) {
// ...
const options = {
serverUrl: serverUrl,
roomId: roomId
};

useEffect(() => {
const connection = createConnection(options);
connection.connect();
// ...

该对象在组件主体中声明,因此它是一个 反应值。 当你在副作用中读取这样的 React 值时,你将其声明为依赖。这可确保你的副作用 “reacts” 适应其变化:

¥This object is declared in the component body, so it’s a reactive value. When you read a reactive value like this inside an Effect, you declare it as a dependency. This ensures your Effect “reacts” to its changes:

// ...
useEffect(() => {
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [options]); // ✅ All dependencies declared
// ...

将其声明为依赖很重要!例如,这可以确保如果 roomId 发生变化,你的副作用将重新连接到与新 options 的聊天。但是,上面的代码也有问题。要查看它,请尝试在下面的沙盒中输入内容,然后观察控制台中发生的情况:

¥It is important to declare it as a dependency! This ensures, for example, that if the roomId changes, your Effect will re-connect to the chat with the new options. However, there is also a problem with the code above. To see it, try typing into the input in the sandbox below, and watch what happens in the console:

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

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

function ChatRoom({ roomId }) {
  const [message, setMessage] = useState('');

  // Temporarily disable the linter to demonstrate the problem
  // eslint-disable-next-line react-hooks/exhaustive-deps
  const options = {
    serverUrl: serverUrl,
    roomId: roomId
  };

  useEffect(() => {
    const connection = createConnection(options);
    connection.connect();
    return () => connection.disconnect();
  }, [options]);

  return (
    <>
      <h1>Welcome to the {roomId} room!</h1>
      <input value={message} onChange={e => setMessage(e.target.value)} />
    </>
  );
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  return (
    <>
      <label>
        Choose the chat room:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">general</option>
          <option value="travel">travel</option>
          <option value="music">music</option>
        </select>
      </label>
      <hr />
      <ChatRoom roomId={roomId} />
    </>
  );
}

在上面的沙箱中,输入仅更新 message 状态变量。从用户的角度来看,这不应该影响聊天连接。但是,每次更新 message 时,你的组件都会重新渲染。当你的组件重新渲染时,其中的代码会从头开始重新运行。

¥In the sandbox above, the input only updates the message state variable. From the user’s perspective, this should not affect the chat connection. However, every time you update the message, your component re-renders. When your component re-renders, the code inside of it runs again from scratch.

每次重新渲染 ChatRoom 组件时,都会从头开始创建一个新的 options 对象。React 发现 options 对象与上次渲染期间创建的 options 对象不同。这就是为什么它会重新同步你的副作用(取决于 options),并且会在你键入时重新连接聊天。

¥A new options object is created from scratch on every re-render of the ChatRoom component. React sees that the options object is a different object from the options object created during the last render. This is why it re-synchronizes your Effect (which depends on options), and the chat re-connects as you type.

此问题仅影响对象和函数。在 JavaScript 中,每个新创建的对象和函数都被认为与其他对象和函数不同。没关系,里面的内容可能是一样的!

¥This problem only affects objects and functions. In JavaScript, each newly created object and function is considered distinct from all the others. It doesn’t matter that the contents inside of them may be the same!

// During the first render
const options1 = { serverUrl: 'https://localhost:1234', roomId: 'music' };

// During the next render
const options2 = { serverUrl: 'https://localhost:1234', roomId: 'music' };

// These are two different objects!
console.log(Object.is(options1, options2)); // false

对象和函数依赖会使你的副作用比你需要的更频繁地重新同步。

¥Object and function dependencies can make your Effect re-synchronize more often than you need.

这就是为什么你应该尽可能避免将对象和函数作为副作用的依赖。而是,尝试将它们移到组件外部、副作用内部,或从中提取原始值。

¥This is why, whenever possible, you should try to avoid objects and functions as your Effect’s dependencies. Instead, try moving them outside the component, inside the Effect, or extracting primitive values out of them.

将静态对象和函数移出组件

¥Move static objects and functions outside your component

如果该对象不依赖于任何属性和状态,你可以将该对象移到你的组件之外:

¥If the object does not depend on any props and state, you can move that object outside your component:

const options = {
serverUrl: 'https://localhost:1234',
roomId: 'music'
};

function ChatRoom() {
const [message, setMessage] = useState('');

useEffect(() => {
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, []); // ✅ All dependencies declared
// ...

这样,你就可以向 linter 证明它不是 React 性的。它不会因为重新渲染而改变,所以它不需要是依赖。现在重新渲染 ChatRoom 不会导致你的副作用器重新同步。

¥This way, you prove to the linter that it’s not reactive. It can’t change as a result of a re-render, so it doesn’t need to be a dependency. Now re-rendering ChatRoom won’t cause your Effect to re-synchronize.

这也适用于功能:

¥This works for functions too:

function createOptions() {
return {
serverUrl: 'https://localhost:1234',
roomId: 'music'
};
}

function ChatRoom() {
const [message, setMessage] = useState('');

useEffect(() => {
const options = createOptions();
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, []); // ✅ All dependencies declared
// ...

由于 createOptions 是在你的组件外部声明的,因此它不是 React 值。这就是为什么它不需要在你的副作用的依赖中指定,以及为什么它永远不会导致你的副作用重新同步。

¥Since createOptions is declared outside your component, it’s not a reactive value. This is why it doesn’t need to be specified in your Effect’s dependencies, and why it won’t ever cause your Effect to re-synchronize.

在副作用中移动动态对象和函数

¥Move dynamic objects and functions inside your Effect

如果你的对象依赖于某些可能因重新渲染而改变的响应值,例如 roomId 属性,则你无法将其拉出你的组件。但是,你可以将其创建移动到副作用的代码中:

¥If your object depends on some reactive value that may change as a result of a re-render, like a roomId prop, you can’t pull it outside your component. You can, however, move its creation inside of your Effect’s code:

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

function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');

useEffect(() => {
const options = {
serverUrl: serverUrl,
roomId: roomId
};
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ All dependencies declared
// ...

现在 options 已在你的副作用中声明,它不再是你的副作用的依赖。而是,你的副作用使用的唯一 React 值是 roomId。由于 roomId 不是对象或函数,因此你可以确定它不会无意中有所不同。在 JavaScript 中,数字和字符串根据它们的内容进行比较:

¥Now that options is declared inside of your Effect, it is no longer a dependency of your Effect. Instead, the only reactive value used by your Effect is roomId. Since roomId is not an object or function, you can be sure that it won’t be unintentionally different. In JavaScript, numbers and strings are compared by their content:

// During the first render
const roomId1 = 'music';

// During the next render
const roomId2 = 'music';

// These two strings are the same!
console.log(Object.is(roomId1, roomId2)); // true

由于此修复,如果你编辑输入,聊天将不再重新连接:

¥Thanks to this fix, the chat no longer re-connects if you edit the input:

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

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

function ChatRoom({ roomId }) {
  const [message, setMessage] = useState('');

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

  return (
    <>
      <h1>Welcome to the {roomId} room!</h1>
      <input value={message} onChange={e => setMessage(e.target.value)} />
    </>
  );
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  return (
    <>
      <label>
        Choose the chat room:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">general</option>
          <option value="travel">travel</option>
          <option value="music">music</option>
        </select>
      </label>
      <hr />
      <ChatRoom roomId={roomId} />
    </>
  );
}

但是,当你更改 roomId 下拉列表时,它确实会重新连接,正如你所期望的那样。

¥However, it does re-connect when you change the roomId dropdown, as you would expect.

这也适用于功能:

¥This works for functions, too:

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

function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');

useEffect(() => {
function createOptions() {
return {
serverUrl: serverUrl,
roomId: roomId
};
}

const options = createOptions();
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ All dependencies declared
// ...

你可以编写自己的函数来对副作用中的逻辑片段进行分组。只要你还在副作用中声明它们,它们就不是 React 值,因此它们不需要是你的副作用的依赖。

¥You can write your own functions to group pieces of logic inside your Effect. As long as you also declare them inside your Effect, they’re not reactive values, and so they don’t need to be dependencies of your Effect.

从对象中读取原始值

¥Read primitive values from objects

有时,你可能会收到来自属性的对象:

¥Sometimes, you may receive an object from props:

function ChatRoom({ options }) {
const [message, setMessage] = useState('');

useEffect(() => {
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [options]); // ✅ All dependencies declared
// ...

这里的风险是父组件会在渲染过程中创建对象:

¥The risk here is that the parent component will create the object during rendering:

<ChatRoom
roomId={roomId}
options={{
serverUrl: serverUrl,
roomId: roomId
}}
/>

这将导致你的副作用在每次父组件重新渲染时重新连接。要解决此问题,请从副作用外部的对象读取信息,并避免对象和函数依赖:

¥This would cause your Effect to re-connect every time the parent component re-renders. To fix this, read information from the object outside the Effect, and avoid having object and function dependencies:

function ChatRoom({ options }) {
const [message, setMessage] = useState('');

const { roomId, serverUrl } = options;
useEffect(() => {
const connection = createConnection({
roomId: roomId,
serverUrl: serverUrl
});
connection.connect();
return () => connection.disconnect();
}, [roomId, serverUrl]); // ✅ All dependencies declared
// ...

逻辑有点重复(你从副作用外部的对象读取一些值,然后在副作用内部创建具有相同值的对象)。但它非常明确地说明了你的副作用实际依赖的信息。如果对象被父组件无意中重新创建,聊天将不会重新连接。但是,如果 options.roomIdoptions.serverUrl 真的不同,聊天将重新连接。

¥The logic gets a little repetitive (you read some values from an object outside an Effect, and then create an object with the same values inside the Effect). But it makes it very explicit what information your Effect actually depends on. If an object is re-created unintentionally by the parent component, the chat would not re-connect. However, if options.roomId or options.serverUrl really are different, the chat would re-connect.

从函数计算原始值

¥Calculate primitive values from functions

同样的方法也适用于函数。例如,假设父组件传递了一个函数:

¥The same approach can work for functions. For example, suppose the parent component passes a function:

<ChatRoom
roomId={roomId}
getOptions={() => {
return {
serverUrl: serverUrl,
roomId: roomId
};
}}
/>

为避免使其成为依赖(并导致它在重新渲染时重新连接),请在副作用外部调用它。这为你提供了不是对象的 roomIdserverUrl 值,你可以从副作用中读取它们:

¥To avoid making it a dependency (and causing it to re-connect on re-renders), call it outside the Effect. This gives you the roomId and serverUrl values that aren’t objects, and that you can read from inside your Effect:

function ChatRoom({ getOptions }) {
const [message, setMessage] = useState('');

const { roomId, serverUrl } = getOptions();
useEffect(() => {
const connection = createConnection({
roomId: roomId,
serverUrl: serverUrl
});
connection.connect();
return () => connection.disconnect();
}, [roomId, serverUrl]); // ✅ All dependencies declared
// ...

这仅适用于 纯粹的 函数,因为它们在渲染期间可以安全调用。如果你的函数是一个事件处理程序,但你不希望它的更改重新同步你的副作用,而是将其封装到副作用事件中。

¥This only works for pure functions because they are safe to call during rendering. If your function is an event handler, but you don’t want its changes to re-synchronize your Effect, wrap it into an Effect Event instead.

回顾

  • 依赖应始终与代码匹配。

    ¥Dependencies should always match the code.

  • 当你对依赖不满意时,你需要编辑的是代码。

    ¥When you’re not happy with your dependencies, what you need to edit is the code.

  • 抑制 linter 会导致非常混乱的错误,你应该始终避免它。

    ¥Suppressing the linter leads to very confusing bugs, and you should always avoid it.

  • 要删除依赖,你需要 “prove” 到不需要的 linter。

    ¥To remove a dependency, you need to “prove” to the linter that it’s not necessary.

  • 如果某些代码应该运行以响应特定的交互,请将该代码移至事件处理程序。

    ¥If some code should run in response to a specific interaction, move that code to an event handler.

  • 如果你的副作用的不同部分因不同原因需要重新运行,请将其拆分为多个副作用。

    ¥If different parts of your Effect should re-run for different reasons, split it into several Effects.

  • 如果你想根据以前的状态更新一些状态,传递一个更新函数。

    ¥If you want to update some state based on the previous state, pass an updater function.

  • 如果你想读取没有 “reacting” 的最新值,请从你的副作用中提取一个副作用事件。

    ¥If you want to read the latest value without “reacting” it, extract an Effect Event from your Effect.

  • 在 JavaScript 中,如果对象和函数是在不同时间创建的,则它们被认为是不同的。

    ¥In JavaScript, objects and functions are considered different if they were created at different times.

  • 尽量避免对象和函数依赖。将它们移到组件外或副作用内。

    ¥Try to avoid object and function dependencies. Move them outside the component or inside the Effect.

挑战 1 / 4:
修复重置间隔

¥Fix a resetting interval

这个副作用设置了一个每秒滴答作响的间隔。你已经注意到发生了一些奇怪的事情:似乎每次滴答时间隔都会被破坏并重新创建。修复代码,使间隔不会不断重新创建。

¥This Effect sets up an interval that ticks every second. You’ve noticed something strange happening: it seems like the interval gets destroyed and re-created every time it ticks. Fix the code so that the interval doesn’t get constantly re-created.

import { useState, useEffect } from 'react';

export default function Timer() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log('✅ Creating an interval');
    const id = setInterval(() => {
      console.log('⏰ Interval tick');
      setCount(count + 1);
    }, 1000);
    return () => {
      console.log('❌ Clearing an interval');
      clearInterval(id);
    };
  }, [count]);

  return <h1>Counter: {count}</h1>
}


React 中文网 - 粤ICP备13048890号