将事件与副作用分开

事件处理程序仅在你再次执行相同的交互时重新运行。与事件处理程序不同,如果副作用读取的某些值(如属性或状态变量)与上次渲染期间的值不同,则副作用会重新同步。有时,你还需要两种行为的混合:响应某些值而不是其他值而重新运行的副作用。此页面将教你如何操作。

¥Event handlers only re-run when you perform the same interaction again. Unlike event handlers, Effects re-synchronize if some value they read, like a prop or a state variable, is different from what it was during the last render. Sometimes, you also want a mix of both behaviors: an Effect that re-runs in response to some values but not others. This page will teach you how to do that.

你将学习到

  • 如何在事件处理程序和副作用之间进行选择

    ¥How to choose between an event handler and an Effect

  • 为什么副作用是 React 性的,而事件处理程序不是

    ¥Why Effects are reactive, and event handlers are not

  • 当你希望副作用的部分代码不响应时该怎么做

    ¥What to do when you want a part of your Effect’s code to not be reactive

  • 什么是副作用事件,以及如何从你的副作用中提取它们

    ¥What Effect Events are, and how to extract them from your Effects

  • 如何使用副作用事件从副作用中读取最新的属性和状态

    ¥How to read the latest props and state from Effects using Effect Events

在事件处理程序和副作用之间进行选择

¥Choosing between event handlers and Effects

首先,让我们回顾一下事件处理程序和副作用之间的区别。

¥First, let’s recap the difference between event handlers and Effects.

假设你正在实现一个聊天室组件。你的要求如下所示:

¥Imagine you’re implementing a chat room component. Your requirements look like this:

  1. 你的组件应该自动连接到选定的聊天室。

    ¥Your component should automatically connect to the selected chat room.

  2. 当你单击 “发送” 按钮时,它应该会向聊天室发送一条消息。

    ¥When you click the “Send” button, it should send a message to the chat.

假设你已经为它们实现了代码,但你不确定将代码放在哪里。你应该使用事件处理程序还是副作用?每次需要回答这个问题时,请考虑 为什么代码需要运行。

¥Let’s say you’ve already implemented the code for them, but you’re not sure where to put it. Should you use event handlers or Effects? Every time you need to answer this question, consider why the code needs to run.

事件处理程序运行以响应特定的交互

¥Event handlers run in response to specific interactions

从用户的角度来看,发送消息应该是因为单击了特定的 “发送” 按钮。如果你在任何其他时间或出于任何其他原因向它们发送消息,用户会非常不高兴。这就是为什么发送消息应该是一个事件处理程序。事件处理程序让你处理特定的交互:

¥From the user’s perspective, sending a message should happen because the particular “Send” button was clicked. The user will get rather upset if you send their message at any other time or for any other reason. This is why sending a message should be an event handler. Event handlers let you handle specific interactions:

function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
// ...
function handleSendClick() {
sendMessage(message);
}
// ...
return (
<>
<input value={message} onChange={e => setMessage(e.target.value)} />
<button onClick={handleSendClick}>Send</button>
</>
);
}

使用事件处理程序,你可以确保只有当用户按下按钮时 sendMessage(message) 才会运行。

¥With an event handler, you can be sure that sendMessage(message) will only run if the user presses the button.

副作用在需要同步时运行

¥Effects run whenever synchronization is needed

回想一下,你还需要保持组件与聊天室的连接。该代码去哪里了?

¥Recall that you also need to keep the component connected to the chat room. Where does that code go?

运行此代码的原因不是某些特定的交互。用户为什么或如何导航到聊天室屏幕并不重要。现在它们正在查看它并可以与之交互,该组件需要保持与所选聊天服务器的连接。即使聊天室组件是你应用的初始屏幕,并且用户根本没有执行任何交互,你仍然需要连接。这就是为什么它是一个副作用:

¥The reason to run this code is not some particular interaction. It doesn’t matter why or how the user navigated to the chat room screen. Now that they’re looking at it and could interact with it, the component needs to stay connected to the selected chat server. Even if the chat room component was the initial screen of your app, and the user has not performed any interactions at all, you would still need to connect. This is why it’s an Effect:

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

使用此代码,你可以确保与当前所选聊天服务器的连接始终处于活动状态,而不管用户执行的特定交互如何。无论用户只是打开了你的应用、选择了一个不同的房间,还是导航到另一个屏幕并返回,你的副作用都会确保该组件将与当前选择的房间保持同步,并且 必要时重新连接。

¥With this code, you can be sure that there is always an active connection to the currently selected chat server, regardless of the specific interactions performed by the user. Whether the user has only opened your app, selected a different room, or navigated to another screen and back, your Effect ensures that the component will remain synchronized with the currently selected room, and will re-connect whenever it’s necessary.

import { useState, useEffect } from 'react';
import { createConnection, sendMessage } 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();
  }, [roomId]);

  function handleSendClick() {
    sendMessage(message);
  }

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

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

反应值和反应逻辑

¥Reactive values and reactive logic

直观地说,你可以说事件处理程序总是被触发 “manually”,例如通过单击按钮。另一方面,副作用是 “automatic”:它们会根据保持同步所需的频率运行和重新运行。

¥Intuitively, you could say that event handlers are always triggered “manually”, for example by clicking a button. Effects, on the other hand, are “automatic”: they run and re-run as often as it’s needed to stay synchronized.

有一种更精确的方法来考虑这个问题。

¥There is a more precise way to think about this.

在组件体内声明的属性、状态和变量称为 反应值。在此示例中,serverUrl 不是电抗值,但 roomIdmessage 是。它们参与渲染数据流:

¥Props, state, and variables declared inside your component’s body are called reactive values. In this example, serverUrl is not a reactive value, but roomId and message are. They participate in the rendering data flow:

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

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

// ...
}

由于重新渲染,像这样的 React 值可能会改变。例如,用户可以编辑 message 或在下拉列表中选择不同的 roomId。事件处理程序和副作用以不同方式响应变化:

¥Reactive values like these can change due to a re-render. For example, the user may edit the message or choose a different roomId in a dropdown. Event handlers and Effects respond to changes differently:

  • 事件处理程序中的逻辑不是反应性的。除非用户再次执行相同的交互(例如单击),否则它不会再次运行。事件处理程序可以在不使用 “reacting” 的情况下读取响应值。

    ¥Logic inside event handlers is not reactive. It will not run again unless the user performs the same interaction (e.g. a click) again. Event handlers can read reactive values without “reacting” to their changes.

  • 副作用内部的逻辑是反应式的。如果你的副作用读取一个反应值, 你必须将其指定为依赖。 然后,如果重新渲染导致该值发生变化,React 将使用新值重新运行你的副作用的逻辑。

    ¥Logic inside Effects is reactive. If your Effect reads a reactive value, you have to specify it as a dependency. Then, if a re-render causes that value to change, React will re-run your Effect’s logic with the new value.

让我们重新审视前面的示例来说明这种差异。

¥Let’s revisit the previous example to illustrate this difference.

事件处理程序内部的逻辑不是反应式的

¥Logic inside event handlers is not reactive

看看这行代码。这个逻辑是否应该是反应性的?

¥Take a look at this line of code. Should this logic be reactive or not?

// ...
sendMessage(message);
// ...

从用户的角度来看,对 message 的更改并不意味着他们想要发送消息。它仅意味着用户正在打字。换句话说,发送消息的逻辑不应该是反应性的。它不应仅因为 反应值 已更改而再次运行。这就是它属于事件处理程序的原因:

¥From the user’s perspective, a change to the message does not mean that they want to send a message. It only means that the user is typing. In other words, the logic that sends a message should not be reactive. It should not run again only because the reactive value has changed. That’s why it belongs in the event handler:

function handleSendClick() {
sendMessage(message);
}

事件处理程序不是反应式的,因此 sendMessage(message) 只会在用户单击“发送”按钮时运行。

¥Event handlers aren’t reactive, so sendMessage(message) will only run when the user clicks the Send button.

副作用内部的逻辑是反应式的

¥Logic inside Effects is reactive

现在让我们回到这些行:

¥Now let’s return to these lines:

// ...
const connection = createConnection(serverUrl, roomId);
connection.connect();
// ...

从用户的角度来看,对 roomId 的更改确实意味着他们想要连接到不同的房间。换句话说,连接到房间的逻辑应该是反应性的。你希望这些代码行以 reactive value 为 “赶上”,并在该值不同时再次运行。这就是它属于副作用的原因:

¥From the user’s perspective, a change to the roomId does mean that they want to connect to a different room. In other words, the logic for connecting to the room should be reactive. You want these lines of code to “keep up” with the reactive value, and to run again if that value is different. That’s why it belongs in an Effect:

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

副作用是 React 性的,因此 createConnection(serverUrl, roomId)connection.connect() 将针对 roomId 的每个不同值运行。你的副作用使聊天连接与当前选定的房间保持同步。

¥Effects are reactive, so createConnection(serverUrl, roomId) and connection.connect() will run for every distinct value of roomId. Your Effect keeps the chat connection synchronized to the currently selected room.

从副作用中提取非反应性逻辑

¥Extracting non-reactive logic out of Effects

当你想将 React 性逻辑与非 React 性逻辑混合时,事情会变得更加棘手。

¥Things get more tricky when you want to mix reactive logic with non-reactive logic.

例如,假设你想在用户连接到聊天时显示通知。你从属性中读取当前主题(深色或浅色),以便以正确的颜色显示通知:

¥For example, imagine that you want to show a notification when the user connects to the chat. You read the current theme (dark or light) from the props so that you can show the notification in the correct color:

function ChatRoom({ roomId, theme }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.on('connected', () => {
showNotification('Connected!', theme);
});
connection.connect();
// ...

但是,theme 是一个 React 值(它可能会因重新渲染而改变),而 副作用读取的每个反应值都必须声明为它的依赖。 现在你必须将 theme 指定为你的副作用的依赖:

¥However, theme is a reactive value (it can change as a result of re-rendering), and every reactive value read by an Effect must be declared as its dependency. Now you have to specify theme as a dependency of your Effect:

function ChatRoom({ roomId, theme }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.on('connected', () => {
showNotification('Connected!', theme);
});
connection.connect();
return () => {
connection.disconnect()
};
}, [roomId, theme]); // ✅ All dependencies declared
// ...

玩一下这个例子,看看你是否能发现这个用户体验的问题:

¥Play with this example and see if you can spot the problem with this user experience:

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

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

function ChatRoom({ roomId, theme }) {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.on('connected', () => {
      showNotification('Connected!', theme);
    });
    connection.connect();
    return () => connection.disconnect();
  }, [roomId, theme]);

  return <h1>Welcome to the {roomId} room!</h1>
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  const [isDark, setIsDark] = 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>
      <label>
        <input
          type="checkbox"
          checked={isDark}
          onChange={e => setIsDark(e.target.checked)}
        />
        Use dark theme
      </label>
      <hr />
      <ChatRoom
        roomId={roomId}
        theme={isDark ? 'dark' : 'light'}
      />
    </>
  );
}

roomId 发生变化时,聊天会如你所料重新连接。但由于 theme 也是一个依赖,每次在深色和浅色主题之间切换时,聊天也会重新连接。那不是很好!

¥When the roomId changes, the chat re-connects as you would expect. But since theme is also a dependency, the chat also re-connects every time you switch between the dark and the light theme. That’s not great!

换句话说,你不希望这条线是 React 性的,即使它在副作用中(它是 React 性的):

¥In other words, you don’t want this line to be reactive, even though it is inside an Effect (which is reactive):

// ...
showNotification('Connected!', theme);
// ...

你需要一种方法将这种非 React 性逻辑与其周围的 React 性副作用分开。

¥You need a way to separate this non-reactive logic from the reactive Effect around it.

声明一个副作用事件

¥Declaring an Effect Event

开发中

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

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

使用一个名为 useEffectEvent 的特殊钩子从副作用中提取这种非响应式逻辑:

¥Use a special Hook called useEffectEvent to extract this non-reactive logic out of your Effect:

import { useEffect, useEffectEvent } from 'react';

function ChatRoom({ roomId, theme }) {
const onConnected = useEffectEvent(() => {
showNotification('Connected!', theme);
});
// ...

这里,onConnected 被称为效果事件。它是效果逻辑的一部分,但它的行为更像是事件处理程序。它里面的逻辑不是 React 式的,它总是 “看到” 你的属性和状态的最新值。

¥Here, onConnected is called an Effect Event. It’s a part of your Effect logic, but it behaves a lot more like an event handler. The logic inside it is not reactive, and it always “sees” the latest values of your props and state.

现在你可以从副作用内部调用 onConnected 副作用事件:

¥Now you can call the onConnected Effect Event from inside your Effect:

function ChatRoom({ roomId, theme }) {
const onConnected = useEffectEvent(() => {
showNotification('Connected!', theme);
});

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

这解决了问题。请注意,你必须从副作用的依赖列表中删除 onConnected。副作用事件不是反应性的,必须从依赖中省略。

¥This solves the problem. Note that you had to remove onConnected from the list of your Effect’s dependencies. Effect Events are not reactive and must be omitted from dependencies.

验证新行为是否按预期工作:

¥Verify that the new behavior works as you would expect:

import { useState, useEffect } from 'react';
import { experimental_useEffectEvent as useEffectEvent } from 'react';
import { createConnection, sendMessage } from './chat.js';
import { showNotification } from './notifications.js';

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

function ChatRoom({ roomId, theme }) {
  const onConnected = useEffectEvent(() => {
    showNotification('Connected!', theme);
  });

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.on('connected', () => {
      onConnected();
    });
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]);

  return <h1>Welcome to the {roomId} room!</h1>
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  const [isDark, setIsDark] = 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>
      <label>
        <input
          type="checkbox"
          checked={isDark}
          onChange={e => setIsDark(e.target.checked)}
        />
        Use dark theme
      </label>
      <hr />
      <ChatRoom
        roomId={roomId}
        theme={isDark ? 'dark' : 'light'}
      />
    </>
  );
}

你可以将副作用事件视为与事件处理程序非常相似。主要区别在于事件处理程序运行以响应用户交互,而副作用事件由你从副作用触发。副作用事件让你在副作用的 React 性和不应 React 性的代码之间进行 “打破链条”。

¥You can think of Effect Events as being very similar to event handlers. The main difference is that event handlers run in response to a user interactions, whereas Effect Events are triggered by you from Effects. Effect Events let you “break the chain” between the reactivity of Effects and code that should not be reactive.

使用副作用事件读取最新的属性和状态

¥Reading latest props and state with Effect Events

开发中

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

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

副作用事件让你可以修复许多你可能想要抑制依赖 linter 的模式。

¥Effect Events let you fix many patterns where you might be tempted to suppress the dependency linter.

例如,假设你有一个副作用来记录页面访问:

¥For example, say you have an Effect to log the page visits:

function Page() {
useEffect(() => {
logVisit();
}, []);
// ...
}

稍后,你将多条路由添加到你的站点。现在,你的 Page 组件收到一个带有当前路径的 url 属性。你想将 url 作为 logVisit 调用的一部分传递,但依赖代码检查抗诉:

¥Later, you add multiple routes to your site. Now your Page component receives a url prop with the current path. You want to pass the url as a part of your logVisit call, but the dependency linter complains:

function Page({ url }) {
useEffect(() => {
logVisit(url);
}, []); // 🔴 React Hook useEffect has a missing dependency: 'url'
// ...
}

想想你想让代码做什么。你希望为不同的 URL 记录单独的访问,因为每个 URL 代表一个不同的页面。换句话说,这个 logVisit 调用应该对 url 有 React。这就是为什么在这种情况下,遵循依赖 linter 并将 url 添加为依赖是有意义的:

¥Think about what you want the code to do. You want to log a separate visit for different URLs since each URL represents a different page. In other words, this logVisit call should be reactive with respect to the url. This is why, in this case, it makes sense to follow the dependency linter, and add url as a dependency:

function Page({ url }) {
useEffect(() => {
logVisit(url);
}, [url]); // ✅ All dependencies declared
// ...
}

现在假设你想在每次页面访问时包括购物车中的商品数量:

¥Now let’s say you want to include the number of items in the shopping cart together with every page visit:

function Page({ url }) {
const { items } = useContext(ShoppingCartContext);
const numberOfItems = items.length;

useEffect(() => {
logVisit(url, numberOfItems);
}, [url]); // 🔴 React Hook useEffect has a missing dependency: 'numberOfItems'
// ...
}

你在副作用中使用了 numberOfItems,因此 linter 要求你将其添加为依赖。但是,你不希望 logVisit 调用对 numberOfItems 有 React。如果用户将东西放入购物车,并且 numberOfItems 发生变化,这并不意味着用户再次访问了该页面。换句话说,访问该页面在某种意义上是一个 “event”。它发生在一个精确的时间点。

¥You used numberOfItems inside the Effect, so the linter asks you to add it as a dependency. However, you don’t want the logVisit call to be reactive with respect to numberOfItems. If the user puts something into the shopping cart, and the numberOfItems changes, this does not mean that the user visited the page again. In other words, visiting the page is, in some sense, an “event”. It happens at a precise moment in time.

将代码分成两部分:

¥Split the code in two parts:

function Page({ url }) {
const { items } = useContext(ShoppingCartContext);
const numberOfItems = items.length;

const onVisit = useEffectEvent(visitedUrl => {
logVisit(visitedUrl, numberOfItems);
});

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

这里,onVisit 是一个副作用事件。它里面的代码不是 React 性的。这就是为什么你可以使用 numberOfItems(或任何其他响应值!)而不必担心它会导致周围的代码在更改时重新执行。

¥Here, onVisit is an Effect Event. The code inside it isn’t reactive. This is why you can use numberOfItems (or any other reactive value!) without worrying that it will cause the surrounding code to re-execute on changes.

另一方面,副作用本身仍然是 React 性的。副作用中的代码使用 url 属性,因此副作用将在每次使用不同的 url 重新渲染后重新运行。反过来,这将调用 onVisit 副作用事件。

¥On the other hand, the Effect itself remains reactive. Code inside the Effect uses the url prop, so the Effect will re-run after every re-render with a different url. This, in turn, will call the onVisit Effect Event.

结果,你会为 url 的每次更改调用 logVisit,并始终读取最新的 numberOfItems。但是,如果 numberOfItems 自行更改,则不会导致任何代码重新运行。

¥As a result, you will call logVisit for every change to the url, and always read the latest numberOfItems. However, if numberOfItems changes on its own, this will not cause any of the code to re-run.

注意

你可能想知道是否可以不带参数调用 onVisit(),并读取其中的 url

¥You might be wondering if you could call onVisit() with no arguments, and read the url inside it:

const onVisit = useEffectEvent(() => {
logVisit(url, numberOfItems);
});

useEffect(() => {
onVisit();
}, [url]);

这可行,但最好将此 url 显式传递给副作用事件。通过将 url 作为参数传递给你的副作用事件,你是说访问具有不同 url 的页面从用户的角度来看构成了一个单独的 “event”。visitedUrl 是发生的 “event” 的一部分:

¥This would work, but it’s better to pass this url to the Effect Event explicitly. By passing url as an argument to your Effect Event, you are saying that visiting a page with a different url constitutes a separate “event” from the user’s perspective. The visitedUrl is a part of the “event” that happened:

const onVisit = useEffectEvent(visitedUrl => {
logVisit(visitedUrl, numberOfItems);
});

useEffect(() => {
onVisit(url);
}, [url]);

由于你的副作用事件明确地为 visitedUrl “asks”,现在你不能意外地从副作用的依赖中删除 url。如果你删除 url 依赖(导致不同的页面访问被计为一次),linter 将警告你。你希望 onVisiturl 具有 React 性,因此不是读取内部的 url(它不会产生 React 性),而是从你的副作用中传递它。

¥Since your Effect Event explicitly “asks” for the visitedUrl, now you can’t accidentally remove url from the Effect’s dependencies. If you remove the url dependency (causing distinct page visits to be counted as one), the linter will warn you about it. You want onVisit to be reactive with regards to the url, so instead of reading the url inside (where it wouldn’t be reactive), you pass it from your Effect.

如果副作用中有一些异步逻辑,这一点就变得尤为重要:

¥This becomes especially important if there is some asynchronous logic inside the Effect:

const onVisit = useEffectEvent(visitedUrl => {
logVisit(visitedUrl, numberOfItems);
});

useEffect(() => {
setTimeout(() => {
onVisit(url);
}, 5000); // Delay logging visits
}, [url]);

在这里,onVisit 内的 url 对应最新的 url(可能已经更改),但 visitedUrl 对应最初导致此副作用(和此 onVisit 调用)运行的 url

¥Here, url inside onVisit corresponds to the latest url (which could have already changed), but visitedUrl corresponds to the url that originally caused this Effect (and this onVisit call) to run.

深入研究

可以改为抑制依赖 linter 吗?

¥Is it okay to suppress the dependency linter instead?

在现有的代码库中,你有时可能会看到像这样抑制的 lint 规则:

¥In the existing codebases, you may sometimes see the lint rule suppressed like this:

function Page({ url }) {
const { items } = useContext(ShoppingCartContext);
const numberOfItems = items.length;

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

useEffectEvent 成为 React 的稳定部分之后,我们建议永远不要抑制 linter。

¥After useEffectEvent becomes a stable part of React, we recommend never suppressing the linter.

抑制该规则的第一个缺点是,当你的副作用需要 “react” 到你在代码中引入的新 React 依赖时,React 将不再警告你。在前面的示例中,你将 url 添加到依赖中,因为 React 会提醒你这样做。如果你禁用 linter,你将不会再收到此类提醒,提醒你以后对该副作用进行任何编辑。这会导致错误。

¥The first downside of suppressing the rule is that React will no longer warn you when your Effect needs to “react” to a new reactive dependency you’ve introduced to your code. In the earlier example, you added url to the dependencies because React reminded you to do it. You will no longer get such reminders for any future edits to that Effect if you disable the linter. This leads to bugs.

这是一个由抑制 linter 引起的令人困惑的错误示例。在此示例中,handleMove 函数应该读取当前 canMove 状态变量值,以确定点是否应该跟随光标。但是,canMove 始终是 handleMove 内部的 true

¥Here is an example of a confusing bug caused by suppressing the linter. In this example, the handleMove function is supposed to read the current canMove state variable value in order to decide whether the dot should follow the cursor. However, canMove is always true inside handleMove.

你能明白为什么吗?

¥Can you see why?

import { useState, useEffect } from 'react';

export default function App() {
  const [position, setPosition] = useState({ x: 0, y: 0 });
  const [canMove, setCanMove] = useState(true);

  function handleMove(e) {
    if (canMove) {
      setPosition({ x: e.clientX, y: e.clientY });
    }
  }

  useEffect(() => {
    window.addEventListener('pointermove', handleMove);
    return () => window.removeEventListener('pointermove', handleMove);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return (
    <>
      <label>
        <input type="checkbox"
          checked={canMove}
          onChange={e => setCanMove(e.target.checked)}
        />
        The dot is allowed to move
      </label>
      <hr />
      <div style={{
        position: 'absolute',
        backgroundColor: 'pink',
        borderRadius: '50%',
        opacity: 0.6,
        transform: `translate(${position.x}px, ${position.y}px)`,
        pointerEvents: 'none',
        left: -20,
        top: -20,
        width: 40,
        height: 40,
      }} />
    </>
  );
}

此代码的问题在于抑制依赖 linter。如果取消抑制,你会看到此副作用器应依赖于 handleMove 函数。这是有道理的:handleMove 在组件体内声明,这使它成为一个 React 值。每个反应值都必须指定为依赖,否则它可能会随着时间的推移而变得失效!

¥The problem with this code is in suppressing the dependency linter. If you remove the suppression, you’ll see that this Effect should depend on the handleMove function. This makes sense: handleMove is declared inside the component body, which makes it a reactive value. Every reactive value must be specified as a dependency, or it can potentially get stale over time!

原始代码的作者通过说副作用不依赖于 ([]) 任何 React 值来对 React 进行 “lied”。这就是为什么在 canMove 发生变化(以及 handleMove 发生变化)后 React 没有重新同步副作用的原因。因为 React 没有重新同步副作用,作为监听器附加的 handleMove 是在初始渲染期间创建的 handleMove 函数。在初始渲染期间,canMovetrue,这就是初始渲染中的 handleMove 将永远看到该值的原因。

¥The author of the original code has “lied” to React by saying that the Effect does not depend ([]) on any reactive values. This is why React did not re-synchronize the Effect after canMove has changed (and handleMove with it). Because React did not re-synchronize the Effect, the handleMove attached as a listener is the handleMove function created during the initial render. During the initial render, canMove was true, which is why handleMove from the initial render will forever see that value.

如果你从不抑制 linter,你将永远不会看到旧值的问题。

¥If you never suppress the linter, you will never see problems with stale values.

使用 useEffectEvent,就不需要将 “lie” 用于 linter,并且代码可以按你预期的方式工作:

¥With useEffectEvent, there is no need to “lie” to the linter, and the code works as you would expect:

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

export default function App() {
  const [position, setPosition] = useState({ x: 0, y: 0 });
  const [canMove, setCanMove] = useState(true);

  const onMove = useEffectEvent(e => {
    if (canMove) {
      setPosition({ x: e.clientX, y: e.clientY });
    }
  });

  useEffect(() => {
    window.addEventListener('pointermove', onMove);
    return () => window.removeEventListener('pointermove', onMove);
  }, []);

  return (
    <>
      <label>
        <input type="checkbox"
          checked={canMove}
          onChange={e => setCanMove(e.target.checked)}
        />
        The dot is allowed to move
      </label>
      <hr />
      <div style={{
        position: 'absolute',
        backgroundColor: 'pink',
        borderRadius: '50%',
        opacity: 0.6,
        transform: `translate(${position.x}px, ${position.y}px)`,
        pointerEvents: 'none',
        left: -20,
        top: -20,
        width: 40,
        height: 40,
      }} />
    </>
  );
}

这并不意味着 useEffectEvent 始终是正确的解决方案。你应该只将它应用于你不想响应的代码行。在上面的沙盒中,你不希望副作用的代码对 canMove 有 React。这就是提取副作用事件有意义的原因。

¥This doesn’t mean that useEffectEvent is always the correct solution. You should only apply it to the lines of code that you don’t want to be reactive. In the above sandbox, you didn’t want the Effect’s code to be reactive with regards to canMove. That’s why it made sense to extract an Effect Event.

阅读 移除副作用依赖 了解抑制 linter 的其他正确替代方法。

¥Read Removing Effect Dependencies for other correct alternatives to suppressing the linter.

副作用事件的限制

¥Limitations of Effect Events

开发中

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

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

副作用事件的使用方式非常有限:

¥Effect Events are very limited in how you can use them:

  • 只能从副作用内部调用它们。

    ¥Only call them from inside Effects.

  • 永远不要将它们传递给其他组件或钩子。

    ¥Never pass them to other components or Hooks.

例如,不要像这样声明和传递副作用事件:

¥For example, don’t declare and pass an Effect Event like this:

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

const onTick = useEffectEvent(() => {
setCount(count + 1);
});

useTimer(onTick, 1000); // 🔴 Avoid: Passing Effect Events

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

function useTimer(callback, delay) {
useEffect(() => {
const id = setInterval(() => {
callback();
}, delay);
return () => {
clearInterval(id);
};
}, [delay, callback]); // Need to specify "callback" in dependencies
}

而是,始终直接在使用它们的副作用旁边声明副作用事件:

¥Instead, always declare Effect Events directly next to the Effects that use them:

function Timer() {
const [count, setCount] = useState(0);
useTimer(() => {
setCount(count + 1);
}, 1000);
return <h1>{count}</h1>
}

function useTimer(callback, delay) {
const onTick = useEffectEvent(() => {
callback();
});

useEffect(() => {
const id = setInterval(() => {
onTick(); // ✅ Good: Only called locally inside an Effect
}, delay);
return () => {
clearInterval(id);
};
}, [delay]); // No need to specify "onTick" (an Effect Event) as a dependency
}

副作用事件是你的副作用代码的非 React 性 “pieces”。它们应该在使用它们的副作用旁边。

¥Effect Events are non-reactive “pieces” of your Effect code. They should be next to the Effect using them.

回顾

  • 事件处理程序运行以响应特定的交互。

    ¥Event handlers run in response to specific interactions.

  • 只要需要同步,副作用就会运行。

    ¥Effects run whenever synchronization is needed.

  • 事件处理程序中的逻辑不是反应性的。

    ¥Logic inside event handlers is not reactive.

  • 副作用内部的逻辑是反应式的。

    ¥Logic inside Effects is reactive.

  • 你可以将非 React 性逻辑从副作用移动到副作用事件 s。

    ¥You can move non-reactive logic from Effects into Effect Events.

  • 仅从副作用内部调用副作用事件 s。

    ¥Only call Effect Events from inside Effects.

  • 不要将副作用事件传递给其他组件或钩子。

    ¥Don’t pass Effect Events to other components or Hooks.

挑战 1 / 4:
修复一个不更新的变量

¥Fix a variable that doesn’t update

这个 Timer 组件保持每秒增加的 count 状态变量。它增加的值存储在 increment 状态变量中。你可以使用加号和减号按钮控制 increment 变量。

¥This Timer component keeps a count state variable which increases every second. The value by which it’s increasing is stored in the increment state variable. You can control the increment variable with the plus and minus buttons.

但是,无论你点击加号按钮多少次,计数器仍然每秒递增 1。这段代码有什么问题?为什么副作用代码中的 increment 总是等于 1?找出错误并改正它。

¥However, no matter how many times you click the plus button, the counter is still incremented by one every second. What’s wrong with this code? Why is increment always equal to 1 inside the Effect’s code? Find the mistake and fix it.

import { useState, useEffect } from 'react';

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

  useEffect(() => {
    const id = setInterval(() => {
      setCount(c => c + increment);
    }, 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>
    </>
  );
}


React 中文网 - 粤ICP备13048890号