事件处理程序仅在你再次执行相同的交互时才会重新运行。与事件处理程序不同的是,如果某些它们读取的值(例如 prop 或状态变量)与上一次渲染时不同,Effect 会重新同步。有时,你也希望两种行为的混合:一个 Effect 对某些值的变化会重新运行,但对其他值则不会。本页将教你如何做到这一点。
🌐 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.
你将学习到
- 如何在事件处理程序和副作用之间进行选择
- 为什么副作用是 React 性的,而事件处理程序不是
- 当你希望副作用的部分代码不响应时该怎么做
- 什么是副作用事件,以及如何从你的副作用中提取它们
- 如何使用副作用事件从副作用中读取最新的属性和状态
在事件处理程序和副作用之间进行选择
🌐 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:
- 你的组件应该自动连接到选定的聊天室。
- 当你点击“发送”按钮时,它应该向聊天发送一条消息。
假设你已经为它们实现了代码,但你不确定应该放在哪里。你应该使用事件处理程序还是 Effects?每次你需要回答这个问题时,请考虑*代码为什么需要运行。
🌐 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?
运行这段代码的原因并不是某种特定的交互。用户为什么或如何导航到聊天室界面并不重要。现在他们正在查看它并可能与之互动,该组件需要保持与所选聊天服务器的连接。即使聊天室组件是你的应用的初始屏幕,并且用户根本没有进行任何交互,你仍然需要连接。这就是为什么要使用 Effect 的原因:
🌐 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]);
// ...
}使用这段代码,你可以确保始终与当前选定的聊天服务器保持活动连接,无论用户执行了哪些具体操作。无论用户只是打开了你的应用、选择了不同的房间,还是导航到其他屏幕再返回,你的 Effect 都能确保组件与当前选定的房间保持同步,并且会在必要时重新连接
🌐 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
直观地说,你可以说事件处理程序总是“手动”触发的,例如通过点击按钮。另一方面,副作用是“自动”的:它们会根据需要不断运行和重新运行,以保持同步。
🌐 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.
在组件主体中声明的 props、state 和变量被称为 响应式值。在此示例中,serverUrl 不是响应式值,而 roomId 和 message 是。它们参与渲染数据流:
const serverUrl = 'https://localhost:1234';
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
// ...
}像这样的响应式值可能会因为重新渲染而发生变化。例如,用户可能会编辑 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:
- 事件处理程序中的逻辑不是响应式的。 除非用户再次执行相同的操作(例如点击),否则它不会再次运行。事件处理程序可以读取响应式值,而不会对其变化“作出反应”。
- Effects 内的逻辑是响应性的。 如果你的 Effect 读取了一个响应式值,你必须将其指定为依赖。 然后,如果重新渲染导致该值发生变化,React 将使用新值重新运行你的 Effect 逻辑。
让我们重新审视前面的示例来说明这种差异。
🌐 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 的更改并不意味着他们想发送消息。 它仅仅意味着用户正在输入。换句话说,发送消息的逻辑不应该是响应式的。它不应该仅仅因为 响应式值 发生了变化而再次运行。这就是为什么它属于事件处理程序的原因:
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 的更改确实意味着他们想要连接到不同的房间。 换句话说,连接到房间的逻辑应该是响应式的。你希望这些代码行能够“跟上” 响应式值,并在该值发生变化时再次运行。这就是为什么它属于一个 Effect 的原因:
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect()
};
}, [roomId]);Effects 是响应性的,因此 createConnection(serverUrl, roomId) 和 connection.connect() 会针对 roomId 的每一个不同值运行。你的 Effect 会保持聊天连接与当前选定的房间同步。
🌐 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.
例如,假设你想在用户连接到聊天时显示通知。你可以从 props 中读取当前主题(深色或浅色),以便以正确的颜色显示通知:
🌐 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 是一个响应式值(它可能会因重新渲染而改变),并且 每个被 Effect 读取的响应式值都必须声明为其依赖。 现在你必须将 theme 指定为你的 Effect 的依赖:
🌐 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!
换句话说,你不希望这行是响应式的,即使它在一个 Effect(这是响应式的)内部:
🌐 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
使用名为 useEffectEvent 的特殊 Hook 将这个非响应式逻辑从你的 Effect 中提取出来:
🌐 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 被称为一个Effect 事件。它是你 Effect 逻辑的一部分,但它的行为更像一个事件处理器。其内部的逻辑不是响应式的,并且它总是“看到”你 props 和 state 的最新值。
🌐 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
// ...这解决了问题。请注意,你必须从 Effect 的依赖列表中移除 theme,因为它不再在 Effect 中使用。你也不需要添加 onConnected,因为Effect 事件不是响应式的,必须从依赖中省略。
🌐 This solves the problem. Note that you had to remove theme from the list of your Effect’s dependencies, because it’s no longer used in the Effect. You also don’t need to add onConnected to it, because 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 { 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'} /> </> ); }
你可以将效果事件(Effect Events)理解为与事件处理程序非常相似。主要区别在于,事件处理程序是对用户交互作出响应时运行的,而效果事件则是由你从效果中触发的。效果事件让你能够“打破”效果的响应性与不应具有响应性的代码之间的链条。
🌐 You can think of Effect Events as being very similar to event handlers. The main difference is that event handlers run in response to 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
副作用事件让你可以修复许多你可能想要抑制依赖 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具有响应性。这就是为什么在这种情况下,遵循依赖检查器是有意义的,并将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'
// ...
}你在 Effect 中使用了 numberOfItems,所以 linter 会要求你将其添加为依赖。然而,你 不 想让 logVisit 的调用对 numberOfItems 产生响应。如果用户将某些东西放入购物车,而 numberOfItems 发生了变化,这 并不意味着 用户再次访问了页面。换句话说,访问页面 在某种意义上是一种“事件”。它发生在一个精确的时间点。
🌐 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 是一个效果事件。它内部的代码不是响应式的。这就是为什么你可以使用 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.
另一方面,Effect 本身仍然是响应性的。Effect 内的代码使用了 url 属性,因此在每次使用不同 url 重新渲染后,Effect 都会重新运行。这反过来会调用 onVisit Effect 事件。
🌐 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.
深入研究
🌐 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]);
// ...
}我们建议永远不要禁用代码检查工具。
🌐 We recommend never suppressing the linter.
抑制该规则的第一个缺点是,当你的 Effect 需要对你在代码中引入的新响应式依赖进行“响应”时,React 将不再提醒你。在前面的示例中,你之所以将 url 添加到依赖,是因为 React 提醒你这样做。如果你禁用 lint 工具,将不会再收到针对该 Effect 的任何未来修改的此类提醒。这会导致错误。
🌐 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 状态变量值,以决定点是否应该跟随光标。然而,在 handleMove 内,canMove 总是 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, }} /> </> ); }
这段代码的问题在于抑制依赖检查器。如果你移除抑制,你会看到这个 Effect 应该依赖于 handleMove 函数。这很有道理:handleMove 在组件体内声明,这使它成为一个响应式值。每个响应式值都必须被指定为依赖,否则它可能会随着时间的推移变得过时!
🌐 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!
原始代码的作者通过说这个 Effect 不依赖于任何响应式值([])而对 React“撒谎”。这就是为什么在 canMove 改变后(以及随之变化的 handleMove)React 没有重新同步该 Effect 的原因。因为 React 没有重新同步 Effect,附加为监听器的 handleMove 是在初始渲染期间创建的 handleMove 函数。在初始渲染期间,canMove 是 true,这就是为什么初始渲染的 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.
如果你从不禁用代码检查工具,你将永远不会看到过时值的问题。
使用 useEffectEvent,不需要对 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 { 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 总是 正确的解决方案。你应该只将它应用于那些你不希望具有响应性的代码行。在上面的沙盒中,你不希望 Effect 的代码对于 canMove 具有响应性。这就是提取 Effect 事件有意义的原因。
🌐 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.
阅读 Removing Effect Dependencies 以了解抑制 linter 的其他正确方法。
🌐 Read Removing Effect Dependencies for other correct alternatives to suppressing the linter.
副作用事件的限制
🌐 Limitations of Effect Events
副作用事件的使用方式非常有限:
🌐 Effect Events are very limited in how you can use them:
- 只能从效果内部调用它们。
- 绝不要将它们传递给其他组件或 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
}效果事件是你的效果代码中非反应性的“片段”。它们应该放在使用它们的效果旁边。
🌐 Effect Events are non-reactive “pieces” of your Effect code. They should be next to the Effect using them.
回顾
- 事件处理程序运行以响应特定的交互。
- 只要需要同步,副作用就会运行。
- 事件处理程序中的逻辑不是反应性的。
- 副作用内部的逻辑是反应式的。
- 你可以将非 React 性逻辑从副作用移动到副作用事件 s。
- 仅从副作用内部调用副作用事件 s。
- 不要将副作用事件传递给其他组件或钩子。
挑战 1 of 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.
然而,无论你点击加号按钮多少次,计数器每秒仍然只增加一。这个代码有什么问题?为什么在 Effect 的代码中 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> </> ); }