Some components need to synchronize with external systems. For example, you might want to control a non-React component based on the React state, set up a server connection, or send an analytics log when a component appears on the screen. Effects let you run some code after rendering so that you can synchronize your component with some system outside of React. 有些组件需要与外部系统同步。例如,你可能希望根据 React state 控制非 React 组件、建立服务器连接或当组件在页面显示时发送分析日志。Effect 允许你在渲染结束后执行一些代码,以便将组件与 React 外部的某个系统相同步。
你将会学习到
- What Effects are
- 什么是 Effect
- How Effects are different from events
- Effect 与事件(event)有何不同
- How to declare an Effect in your component
- 如何在组件中声明 Effect
- How to skip re-running an Effect unnecessarily
- 如何避免不必要地重新运行 Effect
- Why Effects run twice in development and how to fix them
- 为什么 Effect 在开发环境中会运行两次以及如何解决这个问题
什么是 Effect,它与事件(event)有何不同?| What are Effects and how are they different from events?
Before getting to Effects, you need to be familiar with two types of logic inside React components: 在接触 Effect 之前,你需要熟悉 React 组件中的两种逻辑类型:
-
Rendering code (introduced in Describing the UI) lives at the top level of your component. This is where you take the props and state, transform them, and return the JSX you want to see on the screen. Rendering code must be pure. Like a math formula, it should only calculate the result, but not do anything else.
-
渲染代码(在 描述 UI 中有介绍)位于组件的顶层。你在这里处理 props 和 state,对它们进行转换,并返回希望在页面上显示的 JSX。渲染代码必须是纯粹的——就像数学公式一样,它只应该“计算”结果,而不做其他任何事情。
-
Event handlers (introduced in Adding Interactivity) are nested functions inside your components that do things rather than just calculate them. An event handler might update an input field, submit an HTTP POST request to buy a product, or navigate the user to another screen. Event handlers contain “side effects” (they change the program’s state) caused by a specific user action (for example, a button click or typing).
-
事件处理程序(在 添加交互性 中有介绍)是组件内部的嵌套函数,它们不光进行计算, 还会执行一些操作。事件处理程序可能会更新输入字段、提交 HTTP POST 请求来购买产品,或者将用户导航到另一个页面。事件处理程序包含由特定用户操作(例如按钮点击或输入)引起的“副作用”(它们改变了程序的状态)。
Sometimes this isn’t enough. Consider a ChatRoom
component that must connect to the chat server whenever it’s visible on the screen. Connecting to a server is not a pure calculation (it’s a side effect) so it can’t happen during rendering. However, there is no single particular event like a click that causes ChatRoom
to be displayed.
有时这还不够。考虑一个 ChatRoom
组件,它在页面上显示时必须连接到聊天服务器。连接到服务器并不是纯粹的计算(它是一个副作用),因此它不能在渲染期间发生。然而,并没有一个特定的事件(比如点击)能让 ChatRoom
被显示。
Effects let you specify side effects that are caused by rendering itself, rather than by a particular event. Sending a message in the chat is an event because it is directly caused by the user clicking a specific button. However, setting up a server connection is an Effect because it should happen no matter which interaction caused the component to appear. Effects run at the end of a commit after the screen updates. This is a good time to synchronize the React components with some external system (like network or a third-party library). Effect 允许你指定由渲染自身,而不是特定事件引起的副作用。在聊天中发送消息是一个“事件”,因为它直接由用户点击特定按钮引起。然而,建立服务器连接是一个 Effect,因为无论哪种交互致使组件出现,它都应该发生。Effect 在 提交 结束后、页面更新后运行。此时是将 React 组件与外部系统(如网络或第三方库)同步的最佳时机。
你可能不需要 Effect | You might not need an Effect
Don’t rush to add Effects to your components. Keep in mind that Effects are typically used to “step out” of your React code and synchronize with some external system. This includes browser APIs, third-party widgets, network, and so on. If your Effect only adjusts some state based on other state, you might not need an Effect. 不要急着在你的组件中使用 Effect。记住,Effect 通常用于暂时“跳出” React 并与一些 外部 系统进行同步。这包括浏览器 API、第三方小部件,以及网络等等。如果你的 Effect 只是根据其他状态来调整某些状态,那么 你可能并不需要一个 Effect。
如何编写 Effect | How to write an Effect
To write an Effect, follow these three steps: 要编写一个 Effect, 请遵循以下三个步骤:
- Declare an Effect. By default, your Effect will run after every commit.
- 声明 Effect。通常 Effect 会在每次 提交 后运行。
- Specify the Effect dependencies. Most Effects should only re-run when needed rather than after every render. For example, a fade-in animation should only trigger when a component appears. Connecting and disconnecting to a chat room should only happen when the component appears and disappears, or when the chat room changes. You will learn how to control this by specifying dependencies.
- 指定 Effect 依赖。大多数 Effect 应该按需运行,而不是在每次渲染后都运行。例如,淡入动画应该只在组件出现时触发。连接和断开服务器的操作只应在组件出现和消失时,或者切换聊天室时执行。你将通过指定 依赖项 来学习如何控制这一点。
- Add cleanup if needed. Some Effects need to specify how to stop, undo, or clean up whatever they were doing. For example, “connect” needs “disconnect”, “subscribe” needs “unsubscribe”, and “fetch” needs either “cancel” or “ignore”. You will learn how to do this by returning a cleanup function.
- 必要时添加清理操作。一些 Effect 需要指定如何停止、撤销,或者清除它们所执行的操作。例如,“连接”需要“断开”,“订阅”需要“退订”,而“获取数据”需要“取消”或者“忽略”。你将学习如何通过返回一个 清理函数 来实现这些。
Let’s look at each of these steps in detail. 让我们详细看看每一步。
第一步:声明 Effect | Step 1: Declare an Effect
To declare an Effect in your component, import the useEffect
Hook from React:
先从 React 中导入 useEffect
Hook:
import { useEffect } from 'react';
Then, call it at the top level of your component and put some code inside your Effect: 再在组件顶部调用, 并在其中加入一些代码:
function MyComponent() {
useEffect(() => {
// Code here will run after *every* render
// 每次渲染后都会执行此处的代码
});
return <div />;
}
Every time your component renders, React will update the screen and then run the code inside useEffect
. In other words, useEffect
“delays” a piece of code from running until that render is reflected on the screen.
每当你的组件渲染时,React 会先更新页面,然后再运行 useEffect
中的代码。换句话说,useEffect
会“延迟”一段代码的运行,直到渲染结果反映在页面上。
Let’s see how you can use an Effect to synchronize with an external system. Consider a <VideoPlayer>
React component. It would be nice to control whether it’s playing or paused by passing an isPlaying
prop to it:
接下来,让我们看看如何使用 Effect 来与外部系统同步。考虑一个 <VideoPlayer>
React 组件。我们想要通过传递一个 isPlaying
prop 来控制它播放或者暂停:
<VideoPlayer isPlaying={isPlaying} />;
Your custom VideoPlayer
component renders the built-in browser <video>
tag:
这个 VideoPlayer
组件渲染了浏览器内置的 <video>
标签:
function VideoPlayer({ src, isPlaying }) {
// TODO: do something with isPlaying
// TODO:使用 isPlaying 做一些事情
return <video src={src} />;
}
However, the browser <video>
tag does not have an isPlaying
prop. The only way to control it is to manually call the play()
and pause()
methods on the DOM element. You need to synchronize the value of isPlaying
prop, which tells whether the video should currently be playing, with calls like play()
and pause()
.
但是,浏览器的 <video>
标签没有 isPlaying
属性。控制它的唯一方式是在 DOM 元素上调用 play()
和 pause()
方法。因此,你需要将 isPlaying
prop 的值(表示视频当前是否应该播放)与 play()
和 pause()
等函数的调用进行同步。
We’ll need to first get a ref to the <video>
DOM node.
我们首先需要获取 <video>
DOM 节点的 对象引用。
You might be tempted to try to call play()
or pause()
during rendering, but that isn’t correct:
你可能会尝试在渲染期间调用 play()
或 pause()
,但这样做是不对的:
import { useState, useRef, useEffect } from 'react'; function VideoPlayer({ src, isPlaying }) { const ref = useRef(null); if (isPlaying) { ref.current.play(); // 渲染期间不能调用 `play()`。 // Calling these while rendering isn't allowed. } else { ref.current.pause(); // 同样,调用 `pause()` 也不行。 // Also, this crashes. } return <video ref={ref} src={src} loop playsInline />; } export default function App() { const [isPlaying, setIsPlaying] = useState(false); return ( <> <button onClick={() => setIsPlaying(!isPlaying)}> {isPlaying ? '暂停' : '播放'} </button> <VideoPlayer isPlaying={isPlaying} src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4" /> </> ); }
The reason this code isn’t correct is that it tries to do something with the DOM node during rendering. In React, rendering should be a pure calculation of JSX and should not contain side effects like modifying the DOM. 这段代码之所以不对,是因为它试图在渲染期间对 DOM 节点进行操作。在 React 中,渲染应该是纯粹的计算 JSX,不应该包含任何像修改 DOM 这样的副作用。
Moreover, when VideoPlayer
is called for the first time, its DOM does not exist yet! There isn’t a DOM node yet to call play()
or pause()
on, because React doesn’t know what DOM to create until you return the JSX.
而且,当第一次调用 VideoPlayer
时,对应的 DOM 节点还不存在!因为 React 在你返回 JSX 之前不知道要创建什么样的 DOM,所以没有 DOM 节点可以调用 play()
或 pause()
方法。
The solution here is to wrap the side effect with useEffect
to move it out of the rendering calculation:
解决办法是 使用 useEffect
包裹副作用,把它分离到渲染逻辑的计算过程之外:
import { useEffect, useRef } from 'react';
function VideoPlayer({ src, isPlaying }) {
const ref = useRef(null);
useEffect(() => {
if (isPlaying) {
ref.current.play();
} else {
ref.current.pause();
}
});
return <video ref={ref} src={src} loop playsInline />;
}
By wrapping the DOM update in an Effect, you let React update the screen first. Then your Effect runs. 通过将 DOM 更新封装在 Effect 中,你可以让 React 先更新页面,然后再运行 Effect。
When your VideoPlayer
component renders (either the first time or if it re-renders), a few things will happen. First, React will update the screen, ensuring the <video>
tag is in the DOM with the right props. Then React will run your Effect. Finally, your Effect will call play()
or pause()
depending on the value of isPlaying
.
当 VideoPlayer
组件渲染时(无论是否为首次渲染),会发生以下几件事:首先 React 会更新页面,确保 <video>
标签带着正确的 props 出现在 DOM 中;接着 React 将运行 Effect;最后 Effect 将根据 isPlaying
的值调用 play()
或 pause()
。
Press Play/Pause multiple times and see how the video player stays synchronized to the isPlaying
value:
试试点击几次播放和暂停按钮,观察视频播放器的行为是如何与 isPlaying
的值相同步的:
import { useState, useRef, useEffect } from 'react'; function VideoPlayer({ src, isPlaying }) { const ref = useRef(null); useEffect(() => { if (isPlaying) { ref.current.play(); } else { ref.current.pause(); } }); return <video ref={ref} src={src} loop playsInline />; } export default function App() { const [isPlaying, setIsPlaying] = useState(false); return ( <> <button onClick={() => setIsPlaying(!isPlaying)}> {isPlaying ? '暂停' : '播放'} </button> <VideoPlayer isPlaying={isPlaying} src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4" /> </> ); }
In this example, the “external system” you synchronized to React state was the browser media API. You can use a similar approach to wrap legacy non-React code (like jQuery plugins) into declarative React components. 在这个示例中,你同步到 React state 的“外部系统”是浏览器媒体 API。你也可以使用类似的方法将传统的非 React 代码(如 jQuery 插件)封装成声明式的 React 组件。
Note that controlling a video player is much more complex in practice. Calling play()
may fail, the user might play or pause using the built-in browser controls, and so on. This example is very simplified and incomplete.
需要注意的是,控制视频播放器在实际应用中要复杂得多:比如调用 play()
可能会失败、用户可能会使用内置的浏览器控件来进行播放或暂停等操作。本例子是一个非常简化且不完整的示例。
第二步:指定 Effect 的依赖项 | Step 2: Specify the Effect dependencies
By default, Effects run after every render. Often, this is not what you want: 默认情况下,Effect 会在 每次 渲染后运行。但往往 这并不是你想要的:
- Sometimes, it’s slow. Synchronizing with an external system is not always instant, so you might want to skip doing it unless it’s necessary. For example, you don’t want to reconnect to the chat server on every keystroke.
- 有时,它可能会很慢。与外部系统的同步并不总是即时的,所以你可能希望在不必要时跳过它。例如,你不会想在每次打字时都得重新连接聊天服务器。
- Sometimes, it’s wrong. For example, you don’t want to trigger a component fade-in animation on every keystroke. The animation should only play once when the component appears for the first time.
- 有时,它可能会出错。例如,你不会想在每次按键时都触发组件的淡入动画。动画应该只在组件首次出现时播放。
To demonstrate the issue, here is the previous example with a few console.log
calls and a text input that updates the parent component’s state. Notice how typing causes the Effect to re-run:
为了演示这个问题,以下是在之前的示例中加入了一些 console.log
调用和一个更新父组件 state 的文本输入框。注意在输入时是如何触发 Effect 重新运行的:
import { useState, useRef, useEffect } from 'react'; function VideoPlayer({ src, isPlaying }) { const ref = useRef(null); useEffect(() => { if (isPlaying) { console.log('调用 video.play()'); ref.current.play(); } else { console.log('调用 video.pause()'); ref.current.pause(); } }); return <video ref={ref} src={src} loop playsInline />; } export default function App() { const [isPlaying, setIsPlaying] = useState(false); const [text, setText] = useState(''); return ( <> <input value={text} onChange={e => setText(e.target.value)} /> <button onClick={() => setIsPlaying(!isPlaying)}> {isPlaying ? '暂停' : '播放'} </button> <VideoPlayer isPlaying={isPlaying} src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4" /> </> ); }
You can tell React to skip unnecessarily re-running the Effect by specifying an array of dependencies as the second argument to the useEffect
call. Start by adding an empty []
array to the above example on line 14:
通过在调用 useEffect
时指定一个 依赖数组 作为第二个参数,你可以让 React 跳过不必要地重新运行 Effect。首先,在上面示例的第 14 行中传入一个空数组 []
:
useEffect(() => {
// ...
}, []);
You should see an error saying React Hook useEffect has a missing dependency: 'isPlaying'
:
你会看到一个错误提示:React Hook useEffect has a missing dependency: 'isPlaying'
:
import { useState, useRef, useEffect } from 'react'; function VideoPlayer({ src, isPlaying }) { const ref = useRef(null); useEffect(() => { if (isPlaying) { console.log('调用 video.play()'); ref.current.play(); } else { console.log('调用 video.pause()'); ref.current.pause(); } }, []); // 这将产生错误 // This causes an error return <video ref={ref} src={src} loop playsInline />; } export default function App() { const [isPlaying, setIsPlaying] = useState(false); const [text, setText] = useState(''); return ( <> <input value={text} onChange={e => setText(e.target.value)} /> <button onClick={() => setIsPlaying(!isPlaying)}> {isPlaying ? '暂停' : '播放'} </button> <VideoPlayer isPlaying={isPlaying} src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4" /> </> ); }
The problem is that the code inside of your Effect depends on the isPlaying
prop to decide what to do, but this dependency was not explicitly declared. To fix this issue, add isPlaying
to the dependency array:
原因在于,你的 Effect 内部代码依赖于 isPlaying
prop 来决定该做什么,但你并没有显式声明这个依赖关系。为了解决这个问题,将 isPlaying
添加至依赖数组中:
useEffect(() => {
if (isPlaying) { // isPlaying 在此处使用…… // It's used here...
// ...
} else {
// ...
}
}, [isPlaying]); // ……所以它必须在此处声明! // ...so it must be declared here!
Now all dependencies are declared, so there is no error. Specifying [isPlaying]
as the dependency array tells React that it should skip re-running your Effect if isPlaying
is the same as it was during the previous render. With this change, typing into the input doesn’t cause the Effect to re-run, but pressing Play/Pause does:
现在所有的依赖都已经声明,所以没有错误了。指定 [isPlaying]
作为依赖数组会告诉 React:如果 isPlaying
与上次渲染时相同,就跳过重新运行 Effect。这样一来,输入框的输入不会触发 Effect 重新运行,只有按下播放/暂停按钮会触发。
import { useState, useRef, useEffect } from 'react'; function VideoPlayer({ src, isPlaying }) { const ref = useRef(null); useEffect(() => { if (isPlaying) { console.log('调用 video.play()'); ref.current.play(); } else { console.log('调用 video.pause()'); ref.current.pause(); } }, [isPlaying]); return <video ref={ref} src={src} loop playsInline />; } export default function App() { const [isPlaying, setIsPlaying] = useState(false); const [text, setText] = useState(''); return ( <> <input value={text} onChange={e => setText(e.target.value)} /> <button onClick={() => setIsPlaying(!isPlaying)}> {isPlaying ? '暂停' : '播放'} </button> <VideoPlayer isPlaying={isPlaying} src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4" /> </> ); }
The dependency array can contain multiple dependencies. React will only skip re-running the Effect if all of the dependencies you specify have exactly the same values as they had during the previous render. React compares the dependency values using the Object.is
comparison. See the useEffect
reference for details.
依赖数组可以包含多个依赖项。只有当你指定的 所有 依赖项的值都与上一次渲染时完全相同,React 才会跳过重新运行该 Effect。React 使用 Object.is
来比较依赖项的值。有关详细信息,请参阅 useEffect
参考文档。
Notice that you can’t “choose” your dependencies. You will get a lint error if the dependencies you specified don’t match what React expects based on the code inside your Effect. This helps catch many bugs in your code. If you don’t want some code to re-run, edit the Effect code itself to not “need” that dependency. 请注意,你不能随意“选择”依赖项。如果你指定的依赖项与 React 根据 Effect 内部代码所推断出的依赖项不匹配,你将收到来自 lint 的错误提示。这有助于捕捉代码中的许多 bug。如果你不希望某些代码重新运行,那么你应当 修改 Effect 代码本身,使其不再“需要”该依赖项。
深入探讨
This Effect uses both ref
and isPlaying
, but only isPlaying
is declared as a dependency:
下面的 Effect 同时使用了 ref
与 isPlaying
prop,但是只有 isPlaying
被声明为依赖项:
function VideoPlayer({ src, isPlaying }) {
const ref = useRef(null);
useEffect(() => {
if (isPlaying) {
ref.current.play();
} else {
ref.current.pause();
}
}, [isPlaying]);
This is because the ref
object has a stable identity: React guarantees you’ll always get the same object from the same useRef
call on every render. It never changes, so it will never by itself cause the Effect to re-run. Therefore, it does not matter whether you include it or not. Including it is fine too:
这是因为 ref
具有 稳定 的标识:React 确保你在 每轮渲染中调用同一个 useRef
时,总能获得相同的对象。ref 不会改变,所以它不会导致重新运行 Effect。因此,在依赖数组中它可有可无。把它加进去也可以:
function VideoPlayer({ src, isPlaying }) {
const ref = useRef(null);
useEffect(() => {
if (isPlaying) {
ref.current.play();
} else {
ref.current.pause();
}
}, [isPlaying, ref]);
The set
functions returned by useState
also have stable identity, so you will often see them omitted from the dependencies too. If the linter lets you omit a dependency without errors, it is safe to do.
useState
返回的 set
函数 也具有稳定的标识,因此它们通常也会被省略。如果在省略某个依赖项时 linter 不会报错,那么这么做就是安全的。
Omitting always-stable dependencies only works when the linter can “see” that the object is stable. For example, if ref
was passed from a parent component, you would have to specify it in the dependency array. However, this is good because you can’t know whether the parent component always passes the same ref, or passes one of several refs conditionally. So your Effect would depend on which ref is passed.
省略始终稳定的依赖项仅在 linter 能“看到”对象是稳定的时候才有效。例如,如果 ref
是从父组件传递过来的,则必须在依赖数组中指定它。这很有必要,因为你无法确定父组件是一直传递相同的 ref,还是根据条件传递不同的 ref。所以,你的 Effect 会依赖于被传递的是哪个 ref。
第三步:按需添加清理(cleanup)函数 | Step 3: Add cleanup if needed
Consider a different example. You’re writing a ChatRoom
component that needs to connect to the chat server when it appears. You are given a createConnection()
API that returns an object with connect()
and disconnect()
methods. How do you keep the component connected while it is displayed to the user?
考虑一个不同的例子。假如你正在编写一个 ChatRoom
组件,该组件在显示时需要连接到聊天服务器。现在为你提供了 createConnection()
API,该 API 返回一个包含 connect()
与 disconnection()
方法的对象。如何确保组件在显示时始终保持连接?
Start by writing the Effect logic: 从编写 Effect 的逻辑开始:
useEffect(() => {
const connection = createConnection();
connection.connect();
});
It would be slow to connect to the chat after every re-render, so you add the dependency array: 如果每次重新渲染后都得进行连接,这会很慢,所以你需要添加依赖数组:
useEffect(() => {
const connection = createConnection();
connection.connect();
}, []);
The code inside the Effect does not use any props or state, so your dependency array is []
(empty). This tells React to only run this code when the component “mounts”, i.e. appears on the screen for the first time.
由于 Effect 中的代码没有使用任何 props 或 state,所以依赖数组为空数组 []
。这告诉 React 仅在组件“挂载”(即首次显示在页面上)时运行此代码。
Let’s try running this code: 试试运行下面的代码:
import { useEffect } from 'react'; import { createConnection } from './chat.js'; export default function ChatRoom() { useEffect(() => { const connection = createConnection(); connection.connect(); }, []); return <h1>欢迎来到聊天室!</h1>; }
This Effect only runs on mount, so you might expect "✅ Connecting..."
to be printed once in the console. However, if you check the console, "✅ Connecting..."
gets printed twice. Why does it happen?
这里的 Effect 仅在组件挂载时运行,所以你可能以为 "✅ 连接中……"
只会在控制台中被打印一次。然而实际情况是 "✅ 连接中……"
被打印了两次!为什么会这样?
Imagine the ChatRoom
component is a part of a larger app with many different screens. The user starts their journey on the ChatRoom
page. The component mounts and calls connection.connect()
. Then imagine the user navigates to another screen—for example, to the Settings page. The ChatRoom
component unmounts. Finally, the user clicks Back and ChatRoom
mounts again. This would set up a second connection—but the first connection was never destroyed! As the user navigates across the app, the connections would keep piling up.
假设 ChatRoom
组件是一个大型多页面应用中的一部分。用户最初在 ChatRoom
页面上。组件挂载并调用 connection.connect()
。接着用户可能会导航到另一个页面,比如切换到“设置”页面,于是 ChatRoom
组件被卸载。最后,当用户点击“返回”时,ChatRoom
组件再次挂载。这将建立第二个连接——但第一个连接从未被销毁!随着用户在应用中来回切换,连接将会不断累积。
Bugs like this are easy to miss without extensive manual testing. To help you spot them quickly, in development React remounts every component once immediately after its initial mount. 这类 bug 在没有大量手动测试的情况下很容易被忽略。为了帮助你快速发现它们,在开发环境中,React 会在组件首次挂载后立即重新挂载一次。
Seeing the "✅ Connecting..."
log twice helps you notice the real issue: your code doesn’t close the connection when the component unmounts.
两次出现 "✅ 连接中……"
能够帮助你注意到真正的问题:在代码中,组件被卸载时没有关闭连接。
To fix the issue, return a cleanup function from your Effect: 为了解决这个问题,可以在 Effect 中返回一个 清理(cleanup)函数 。
useEffect(() => {
const connection = createConnection();
connection.connect();
return () => {
connection.disconnect();
};
}, []);
React will call your cleanup function each time before the Effect runs again, and one final time when the component unmounts (gets removed). Let’s see what happens when the cleanup function is implemented: React 会在每次 Effect 重新运行之前调用清理函数,并在组件卸载(被移除)时最后一次调用清理函数。让我们看看实现清理函数后会发生什么:
import { useState, useEffect } from 'react'; import { createConnection } from './chat.js'; export default function ChatRoom() { useEffect(() => { const connection = createConnection(); connection.connect(); return () => connection.disconnect(); }, []); return <h1>欢迎来到聊天室!</h1>; }
Now you get three console logs in development: 现在在开发环境下,你会看到三条控制台日志:
"✅ 连接中……"
"❌ 连接断开。"
"✅ 连接中……"
This is the correct behavior in development. By remounting your component, React verifies that navigating away and back would not break your code. Disconnecting and then connecting again is exactly what should happen! When you implement the cleanup well, there should be no user-visible difference between running the Effect once vs running it, cleaning it up, and running it again. There’s an extra connect/disconnect call pair because React is probing your code for bugs in development. This is normal—don’t try to make it go away! 在开发环境下,这是正确的行为。通过重新挂载你的组件,React 验证了离开页面再返回不会导致代码出错。因为本就应该先断开然后再重新连接!如果你很好地实现了清理函数,那么无论是只执行一次 Effect ,还是执行、清理、再执行,都应该没有用户可见的区别。之所以会有额外的一次 connect/disconnect 调用,是因为在开发环境下 React 在检测你代码中的 bug。因此这是正常现象,不要去试图消除它!
In production, you would only see "✅ Connecting..."
printed once. Remounting components only happens in development to help you find Effects that need cleanup. You can turn off Strict Mode to opt out of the development behavior, but we recommend keeping it on. This lets you find many bugs like the one above.
在生产环境下,你只会看到 "✅ 连接中……"
打印一次。这是因为重新挂载组件只会在开发环境下发生,以此帮助你找到需要清理的 Effect。你可以通过关闭 严格模式 来禁用这个行为,但我们建议保留它。它可以帮助你发现许多类似上述的 bug。
如何处理在开发环境下 Effect 运行了两次?| How to handle the Effect firing twice in development?
React intentionally remounts your components in development to find bugs like in the last example. The right question isn’t “how to run an Effect once”, but “how to fix my Effect so that it works after remounting”. React 有意在开发环境下重新挂载你的组件,来找到类似上例中的 bug。你需要思考的不是“如何只运行一次 Effect”,而是“如何修复我的 Effect 来让它在重新挂载后正常运行”。
Usually, the answer is to implement the cleanup function. The cleanup function should stop or undo whatever the Effect was doing. The rule of thumb is that the user shouldn’t be able to distinguish between the Effect running once (as in production) and a setup → cleanup → setup sequence (as you’d see in development). 通常,答案是实现清理函数。清理函数应该停止或撤销 Effect 所做的一切。原则是用户不应该感受到 Effect 只执行一次(在生产环境中)和连续执行“挂载 → 清理 → 挂载”(在开发环境中)之间的区别。
Most of the Effects you’ll write will fit into one of the common patterns below. 你将编写的大多数 Effect 都会符合下列的常见模式之一。
管理非 React 小部件 | Controlling non-React widgets
Sometimes you need to add UI widgets that aren’t written in React. For example, let’s say you’re adding a map component to your page. It has a setZoomLevel()
method, and you’d like to keep the zoom level in sync with a zoomLevel
state variable in your React code. Your Effect would look similar to this:
有时你需要添加不是用 React 实现的 UI 小部件。比如说你想在你的页面添加一个地图组件。它有一个 setZoomLevel()
方法,然后你希望地图的缩放比例和代码中的 zoomLevel
state 保持同步。你的 Effect 应该类似于:
useEffect(() => {
const map = mapRef.current;
map.setZoomLevel(zoomLevel);
}, [zoomLevel]);
Note that there is no cleanup needed in this case. In development, React will call the Effect twice, but this is not a problem because calling setZoomLevel
twice with the same value does not do anything. It may be slightly slower, but this doesn’t matter because it won’t remount needlessly in production.
请注意,这种情况下不需要清理操作。在开发环境中,虽然 React 会调用 Effect 两次,但这没关系,因为用相同的值调用 setZoomLevel
两次不会造成任何影响。虽然在开发环境下它可能会稍微慢一些,但问题不大,因为在生产环境下它不会多余地重新挂载。
Some APIs may not allow you to call them twice in a row. For example, the showModal
method of the built-in <dialog>
element throws if you call it twice. Implement the cleanup function and make it close the dialog:
有些 API 可能不允许你连续调用两次。例如,内置的 <dialog>
元素的 showModal
方法在连续被调用两次时会抛出异常。此时可以通过实现清理函数来使其关闭对话框:
useEffect(() => {
const dialog = dialogRef.current;
dialog.showModal();
return () => dialog.close();
}, []);
In development, your Effect will call showModal()
, then immediately close()
, and then showModal()
again. This has the same user-visible behavior as calling showModal()
once, as you would see in production.
在开发环境中,你的 Effect 会先调用 showModal()
,然后立即调用 close()
,之后再次调用 showModal()
。这与在生产环境中只调用一次 showModal()
的用户可见行为是相同的。
订阅事件 | Subscribing to events
If your Effect subscribes to something, the cleanup function should unsubscribe: 如果你的 Effect 订阅了某些事件,清理函数应退订这些事件:
useEffect(() => {
function handleScroll(e) {
console.log(window.scrollX, window.scrollY);
}
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
In development, your Effect will call addEventListener()
, then immediately removeEventListener()
, and then addEventListener()
again with the same handler. So there would be only one active subscription at a time. This has the same user-visible behavior as calling addEventListener()
once, as in production.
在开发环境中,你的 Effect 会先调用 addEventListener()
,然后立即调用 removeEventListener()
,接着再次使用相同的处理函数调用 addEventListener()
。因此,每次只会有一个有效订阅。这与在生产环境中只调用一次 addEventListener()
所产生的用户可见行为是相同的。
触发动画 | Triggering animations
If your Effect animates something in, the cleanup function should reset the animation to the initial values: 如果你的 Effect 触发了一些动画,清理函数应将动画重置为初始状态:
useEffect(() => {
const node = ref.current;
node.style.opacity = 1; // 触发动画 // Trigger the animation
return () => {
node.style.opacity = 0; // 重置为初始值 // Reset to the initial value
};
}, []);
In development, opacity will be set to 1
, then to 0
, and then to 1
again. This should have the same user-visible behavior as setting it to 1
directly, which is what would happen in production. If you use a third-party animation library with support for tweening, your cleanup function should reset the timeline to its initial state.
在开发环境中,透明度由 1
变为 0
,再变为 1
。这与在生产环境中,直接将其设置为 1
具有相同的用户可见行为。如果你使用了支持补间动画的第三方动画库,你的清理函数应将时间轴重置为初始状态。
获取数据 | Fetching data
If your Effect fetches something, the cleanup function should either abort the fetch or ignore its result: 如果你的 Effect 需要获取数据,清理函数应 中止请求 或忽略其结果:
useEffect(() => {
let ignore = false;
async function startFetching() {
const json = await fetchTodos(userId);
if (!ignore) {
setTodos(json);
}
}
startFetching();
return () => {
ignore = true;
};
}, [userId]);
You can’t “undo” a network request that already happened, but your cleanup function should ensure that the fetch that’s not relevant anymore does not keep affecting your application. If the userId
changes from 'Alice'
to 'Bob'
, cleanup ensures that the 'Alice'
response is ignored even if it arrives after 'Bob'
.
你无法“撤销”已经发生的网络请求,但是你的清理函数应当确保那些不再相关的请求不会继续影响你的应用。如果 userId
从 'Alice'
变为 'Bob'
,那么请确保 'Alice'
的响应数据被忽略,即使它在 'Bob'
之后到达。
In development, you will see two fetches in the Network tab. There is nothing wrong with that. With the approach above, the first Effect will immediately get cleaned up so its copy of the ignore
variable will be set to true
. So even though there is an extra request, it won’t affect the state thanks to the if (!ignore)
check.
在开发环境中,你会在浏览器调试工具的“网络”选项卡中看到两条请求。这是正常的。使用上述方法,第一个 Effect 将立即被清理,所以它的 ignore
变量会被设置为 true
。因此,即使有额外的请求,由于有 if (!ignore)
的检查,也不会影响 state。
In production, there will only be one request. If the second request in development is bothering you, the best approach is to use a solution that deduplicates requests and caches their responses between components: 在生产环境中,只会有一条请求。如果开发环境中的第二次请求给你造成了困扰,最好的办法是使用一个能够对请求去重并缓存响应的方案:
function TodoList() {
const todos = useSomeDataLibrary(`/api/user/${userId}/todos`);
// ...
This will not only improve the development experience, but also make your application feel faster. For example, the user pressing the Back button won’t have to wait for some data to load again because it will be cached. You can either build such a cache yourself or use one of the many alternatives to manual fetching in Effects. 这不仅可以提高开发体验,还可以让你的应用程序响应更快。例如,当用户点击返回按钮时,不用再等待数据重新加载,因为它已经被缓存。你可以自己构建这样的缓存机制,也可以使用很多在 Effect 中手动获取数据的替代方法。
深入探讨
Writing fetch
calls inside Effects is a popular way to fetch data, especially in fully client-side apps. This is, however, a very manual approach and it has significant downsides:
在 Effect 中直接编写 fetch
请求 是一种常见的数据获取方式,特别是在完全客户端渲染的应用中。然而,这种方法非常手动化,并且有明显的弊端:
- Effects don’t run on the server. This means that the initial server-rendered HTML will only include a loading state with no data. The client computer will have to download all JavaScript and render your app only to discover that now it needs to load the data. This is not very efficient.
- Effect 不会在服务端运行。这意味着最初由服务器渲染的 HTML 只会包含加载状态,而没有实际数据。客户端必须先下载所有的 JavaScript 并渲染应用,才会发现它需要加载数据——这并不高效。
- Fetching directly in Effects makes it easy to create “network waterfalls”. You render the parent component, it fetches some data, renders the child components, and then they start fetching their data. If the network is not very fast, this is significantly slower than fetching all data in parallel.
- 直接在 Effect 中进行数据请求,容易产生“网络瀑布(network waterfall)”。首先父组件渲染时请求一些数据,随后渲染子组件,接着子组件开始请求它们的数据。如果网络速度不快,这种方式会比并行获取所有数据慢得多。
- Fetching directly in Effects usually means you don’t preload or cache data. For example, if the component unmounts and then mounts again, it would have to fetch the data again.
- 直接在 Effect 中进行数据请求往往无法预加载或缓存数据。例如,如果组件卸载后重新挂载,它必须重新获取数据。
- It’s not very ergonomic. There’s quite a bit of boilerplate code involved when writing
fetch
calls in a way that doesn’t suffer from bugs like race conditions. - 不够简洁。编写 fecth 请求时为了避免 竞态条件(race condition) 等问题,会需要很多样板代码。
This list of downsides is not specific to React. It applies to fetching data on mount with any library. Like with routing, data fetching is not trivial to do well, so we recommend the following approaches: 这些弊端并不仅限于 React。任何库在组件挂载时进行数据获取都会遇到这些问题。与路由处理一样,要做好数据获取并非易事,因此我们推荐以下方法:
- If you use a framework, use its built-in data fetching mechanism. Modern React frameworks have integrated data fetching mechanisms that are efficient and don’t suffer from the above pitfalls.
- 如果你正在使用 框架 ,请使用其内置的数据获取机制。现代 React 框架集成了高效的数据获取机制,不会出现上述问题。
- Otherwise, consider using or building a client-side cache. Popular open source solutions include React Query, useSWR, and React Router 6.4+. You can build your own solution too, in which case you would use Effects under the hood, but add logic for deduplicating requests, caching responses, and avoiding network waterfalls (by preloading data or hoisting data requirements to routes).
- 否则,请考虑使用或构建客户端缓存。流行的开源解决方案包括 React Query、useSWR 和 React Router v6.4+。你也可以自己构建解决方案:在底层使用 Effect,但添加对请求的去重、缓存响应以及避免网络瀑布(通过预加载数据或将数据请求提升到路由层次)的逻辑。
You can continue fetching data directly in Effects if neither of these approaches suit you. 如果这些方法都不适合你,你可以继续直接在 Effect 中获取数据。
发送分析报告 | Sending analytics
Consider this code that sends an analytics event on the page visit: 考虑以下代码,它在页面访问时发送一个分析事件:
useEffect(() => {
logVisit(url); // 发送 POST 请求 // Sends a POST request
}, [url]);
In development, logVisit
will be called twice for every URL, so you might be tempted to try to fix that. We recommend keeping this code as is. Like with earlier examples, there is no user-visible behavior difference between running it once and running it twice. From a practical point of view, logVisit
should not do anything in development because you don’t want the logs from the development machines to skew the production metrics. Your component remounts every time you save its file, so it logs extra visits in development anyway.
在开发环境中,对于每个 URL,logVisit
都会被调用两次,因此你可能会尝试修复这个问题。我们建议保持不动。与之前示例类似,运行一次还是运行两次,在用户可见的行为上没有区别。从实际角度来看,logVisit
不应该在开发环境中执行任何操作,因为你不会想让开发设备的日志影响生产环境的统计数据。每次保存文件时组件都会重新挂载,因此在开发环境中会记录额外的访问日志。
In production, there will be no duplicate visit logs. 在生产环境中,不会有重复的访问日志。
To debug the analytics events you’re sending, you can deploy your app to a staging environment (which runs in production mode) or temporarily opt out of Strict Mode and its development-only remounting checks. You may also send analytics from the route change event handlers instead of Effects. For more precise analytics, intersection observers can help track which components are in the viewport and how long they remain visible. 为了调试发送的分析事件,你可以将应用部署到一个运行在生产模式下的暂存环境,或者暂时禁用 严格模式 及其仅在开发环境中的重新挂载检查。你还可以在路由更改的事件处理程序中发送分析数据,而不是在 Effect 中发送。对于更精确的分析,可以使用交叉观察器 来跟踪哪些组件位于视口中以及它们保持可见的时间。
不适用于 Effect:初始化应用 | Not an Effect: Initializing the application
Some logic should only run once when the application starts. You can put it outside your components: 某些逻辑应该只在应用启动时运行一次。你可以将它放在组件外部:
if (typeof window !== 'undefined') { // 检查是否在浏览器中运行 // Check if we're running in the browser.
checkAuthToken();
loadDataFromLocalStorage();
}
function App() {
// ……
}
This guarantees that such logic only runs once after the browser loads the page. 这可以确保此类逻辑只在浏览器加载页面后运行一次。
不适用于 Effect:购买商品 | Not an Effect: Buying a product
Sometimes, even if you write a cleanup function, there’s no way to prevent user-visible consequences of running the Effect twice. For example, maybe your Effect sends a POST request like buying a product: 有时,即使你编写了清理函数,也无法避免用户观察到 Effect 运行了两次。比如你的 Effect 发送了一个像购买商品这样的 POST 请求:
useEffect(() => {
// 🔴 Wrong: This Effect fires twice in development, exposing a problem in the code.
// 🔴 错误:此处的 Effect 在开发环境中会触发两次,暴露出代码中的问题。
fetch('/api/buy', { method: 'POST' });
}, []);
You wouldn’t want to buy the product twice. However, this is also why you shouldn’t put this logic in an Effect. What if the user goes to another page and then presses Back? Your Effect would run again. You don’t want to buy the product when the user visits a page; you want to buy it when the user clicks the Buy button. 你肯定不希望购买两次商品。这也是为什么你不应该把这种逻辑放在 Effect 中。如果用户跳转到另一个页面,然后按下“返回”按钮,你的 Effect 就会再次运行。你不希望用户在访问页面时就购买产品,而是在他们点击“购买”按钮时才购买。
Buying is not caused by rendering; it’s caused by a specific interaction. It should run only when the user presses the button. Delete the Effect and move your /api/buy
request into the Buy button event handler:
购买操作并不是由渲染引起的,而是由特定的交互引起的。它应该只在用户按下按钮时执行。因此,它不应该写在 Effect 中,应当把 /api/buy
请求移动到“购买”按钮的事件处理程序中:
function handleClick() {
// ✅ Buying is an event because it is caused by a particular interaction.
// ✅ 购买行为是一个事件,因为它是由特定的交互引起的。
fetch('/api/buy', { method: 'POST' });
}
This illustrates that if remounting breaks the logic of your application, this usually uncovers existing bugs. From a user’s perspective, visiting a page shouldn’t be different from visiting it, clicking a link, then pressing Back to view the page again. React verifies that your components abide by this principle by remounting them once in development. 这说明了如果重新挂载破坏了应用的逻辑,通常便暴露了存在的 bug。对用户而言,访问一个页面不应该与访问页面后点击链接、再按下“返回”按钮查看页面有区别。React 通过在开发环境中重新挂载组件来验证你的组件是否遵守这一原则。
综合以上内容 | Putting it all together
This playground can help you “get a feel” for how Effects work in practice. 这个演练场可以帮助你“感受” Effect 在实际中的工作方式。
This example uses setTimeout
to schedule a console log with the input text to appear three seconds after the Effect runs. The cleanup function cancels the pending timeout. Start by pressing “Mount the component”:
这个例子使用 setTimeout
调度一个日志记录,日志会在 Effect 运行三秒后显示输入的文本。清理函数会取消挂起的延时器。从按下“挂载组件”开始:
import { useState, useEffect } from 'react'; function Playground() { const [text, setText] = useState('a'); useEffect(() => { function onTimeout() { console.log('⏰ ' + text); } console.log('🔵 调度 "' + text + '" 日志'); const timeoutId = setTimeout(onTimeout, 3000); return () => { console.log('🟡 取消 "' + text + '" 日志'); clearTimeout(timeoutId); }; }, [text]); return ( <> <label> 日志内容:{' '} <input value={text} onChange={e => setText(e.target.value)} /> </label> <h1>{text}</h1> </> ); } export default function App() { const [show, setShow] = useState(false); return ( <> <button onClick={() => setShow(!show)}> {show ? '卸载' : '挂载'}组件 </button> {show && <hr />} {show && <Playground />} </> ); }
You will see three logs at first: Schedule "a" log
, Cancel "a" log
, and Schedule "a" log
again. Three second later there will also be a log saying a
. As you learned earlier, the extra schedule/cancel pair is because React remounts the component once in development to verify that you’ve implemented cleanup well.
你首先会看到三条日志:调度 "a" 日志
,取消 "a" 日志
,再一条 调度 "a" 日志
。三秒后,你还会看到一条日志:a
。正如你先前所学,额外的调度/取消日志是因为 React 在开发环境中会重新挂载组件一次,以验证你是否正确实现了清理操作。
Now edit the input to say abc
. If you do it fast enough, you’ll see Schedule "ab" log
immediately followed by Cancel "ab" log
and Schedule "abc" log
. React always cleans up the previous render’s Effect before the next render’s Effect. This is why even if you type into the input fast, there is at most one timeout scheduled at a time. Edit the input a few times and watch the console to get a feel for how Effects get cleaned up.
现在编辑输入框,输入 abc
。如果输入速度足够快,你会看到 调度 "ab" 日志
,紧接着 取消 "ab" 日志
和 调度 "abc" 日志
。React 总是在执行下一轮渲染的 Effect 之前清理上一轮渲染的 Effect。这就是为什么即使你快速输入,最多也只有一个延时器被调度。试试多次编辑输入框,并观察控制台以了解 Effect 是如何被清理的。
Type something into the input and then immediately press “Unmount the component”. Notice how unmounting cleans up the last render’s Effect. Here, it clears the last timeout before it has a chance to fire. 在输入框中输入一些内容,然后立即按下“卸载组件”。注意卸载组件时是如何清理最后一轮渲染的 Effect 的。在这里,它会在最后一个延迟器要触发之前取消它。
Finally, edit the component above and comment out the cleanup function so that the timeouts don’t get cancelled. Try typing abcde
fast. What do you expect to happen in three seconds? Will console.log(text)
inside the timeout print the latest text
and produce five abcde
logs? Give it a try to check your intuition!
最后,在上面的代码中注释掉清理函数,这样延时器就不会被取消。尝试快速输入 abcde
。你觉得三秒后会发生什么?延时器中的 console.log(text)
会打印 最新 的 text
值并生成五条 abcde
日志吗?试试看吧,验证一下你的直觉!
Three seconds later, you should see a sequence of logs (a
, ab
, abc
, abcd
, and abcde
) rather than five abcde
logs. Each Effect “captures” the text
value from its corresponding render. It doesn’t matter that the text
state changed: an Effect from the render with text = 'ab'
will always see 'ab'
. In other words, Effects from each render are isolated from each other. If you’re curious how this works, you can read about closures.
三秒后,你应该会看到一系列的日志:a
、ab
、abc
、abcd
与 abcde
,而不是五条 abcde
日志。这是因为 每个 Effect 都会“捕获”它对应渲染时的 text
值。即使 text
的值发生了变化,渲染时 text = 'ab'
的 Effect 总是会得到 'ab'
。换句话说,每个渲染的 Effect 都是相互独立的。如果你对这种机制感兴趣,可以阅读有关 闭包 的内容。
深入探讨
You can think of useEffect
as “attaching” a piece of behavior to the render output. Consider this Effect:
你可以将 useEffect
理解为“附加”一段行为到渲染输出中。考虑下面这个 Effect:
export default function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(roomId);
connection.connect();
return () => connection.disconnect();
}, [roomId]);
return <h1>欢迎来到 {roomId}!</h1>;
}
Let’s see what exactly happens as the user navigates around the app. 让我们看看当用户在应用程序中切换页面时到底发生了什么。
初次渲染 | Initial render
The user visits <ChatRoom roomId="general" />
. Let’s mentally substitute roomId
with 'general'
:
用户访问 <ChatRoom roomId="general" />
。我们 假设 roomId
的值为 'general'
:
// JSX for the first render (roomId = "general")
// 首次渲染时的 JSX(roomId 为 "general")
return <h1>欢迎来到 general!</h1>;
The Effect is also a part of the rendering output. The first render’s Effect becomes: Effect 也是渲染输出的一部分。首次渲染的 Effect 变为:
// Effect for the first render (roomId = "general")
//首先渲染时的 Effect(roomId 为 "general")
() => {
const connection = createConnection('general');
connection.connect();
return () => connection.disconnect();
},
// Dependencies for the first render (roomId = "general")
// 首次渲染时的依赖项(roomId 为 "general")
['general']
React runs this Effect, which connects to the 'general'
chat room.
React 运行这个 Effect 来连接到 'general'
聊天室。
依赖项相同时的重新渲染 | Re-render with same dependencies
Let’s say <ChatRoom roomId="general" />
re-renders. The JSX output is the same:
假设 <ChatRoom roomId="general" />
重新渲染。输出的 JSX 不变:
// JSX for the second render (roomId = "general")
// 第二次渲染时的 JSX(roomId 为 "general")
return <h1>Welcome to general!</h1>;
React sees that the rendering output has not changed, so it doesn’t update the DOM. React 看到渲染输出没有改变,所以不更新 DOM 。
The Effect from the second render looks like this: 第二次渲染的 Effect 如下所示:
// Effect for the second render (roomId = "general")
// 第二次渲染时的 Effect(roomId 为 "general")
() => {
const connection = createConnection('general');
connection.connect();
return () => connection.disconnect();
},
// Dependencies for the second render (roomId = "general")
// 第二次渲染时的依赖项(roomId 为 "general")
['general']
React compares ['general']
from the second render with ['general']
from the first render. Because all dependencies are the same, React ignores the Effect from the second render. It never gets called.
React 将第二次渲染时的 ['general']
与第一次渲染时的 ['general']
进行比较。因为所有的依赖项都相同,React 忽略第二次渲染时的 Effect。Effect 不会被调用。
依赖项不同时的重新渲染 | Re-render with different dependencies
Then, the user visits <ChatRoom roomId="travel" />
. This time, the component returns different JSX:
接着,用户访问了 <ChatRoom roomId="travel" />
。这一次,组件返回了不同的 JSX:
// JSX for the third render (roomId = "travel")
// 第三次渲染时的 JSX(roomId 为 "travel")
return <h1>欢迎来到 travel!</h1>;
React updates the DOM to change "Welcome to general"
into "Welcome to travel"
.
React 更新 DOM ,将 "欢迎来到 general"
改为 "欢迎来到 travel"
。
The Effect from the third render looks like this: 第三次渲染的 Effect 如下所示:
// Effect for the third render (roomId = "travel")
// 第三次渲染时的 Effect(roomId 为 "travel")
() => {
const connection = createConnection('travel');
connection.connect();
return () => connection.disconnect();
},
// Dependencies for the third render (roomId = "travel")
// 第三次渲染时的依赖项(roomId 为 "travel")
['travel']
React compares ['travel']
from the third render with ['general']
from the second render. One dependency is different: Object.is('travel', 'general')
is false
. The Effect can’t be skipped.
React 将第三次渲染时的 ['travel']
与第二次渲染时的 ['general']
相比较。发现依赖项不同:Object.is('travel', 'general')
为 false
。因此 Effect 不能被跳过。
Before React can apply the Effect from the third render, it needs to clean up the last Effect that did run. The second render’s Effect was skipped, so React needs to clean up the first render’s Effect. If you scroll up to the first render, you’ll see that its cleanup calls disconnect()
on the connection that was created with createConnection('general')
. This disconnects the app from the 'general'
chat room.
在 React 执行第三次渲染的 Effect 之前,它需要清理上一个运行的 Effect。第二次渲染的 Effect 被跳过了。所以 React 需要清理第一次渲染时的 Effect。如果你回看第一次渲染,你会发现第一次渲染时的清理函数所做的事,是在 createConnection('general')
所创建的连接上调用 disconnect()
。也就是从 'general'
聊天室断开连接。
After that, React runs the third render’s Effect. It connects to the 'travel'
chat room.
之后,React 执行第三次渲染的 Effect。它连接到 'travel'
聊天室。
组件卸载 | Unmount
Finally, let’s say the user navigates away, and the ChatRoom
component unmounts. React runs the last Effect’s cleanup function. The last Effect was from the third render. The third render’s cleanup destroys the createConnection('travel')
connection. So the app disconnects from the 'travel'
room.
最后,假设用户离开了当前页面,ChatRoom
组件被卸载。React 执行上一个运行的 Effect 的清理函数,也就是第三次渲染时的 Effect。这个清理函数会销毁 createConnection('travel')
所创建的连接。这样,应用与 travel
房间断开了连接。
仅开发环境下的行为 | Development-only behaviors
When Strict Mode is on, React remounts every component once after mount (state and DOM are preserved). This helps you find Effects that need cleanup and exposes bugs like race conditions early. Additionally, React will remount the Effects whenever you save a file in development. Both of these behaviors are development-only. 开启 严格模式 时,React 在每次挂载组件后都会重新挂载组件(组件的 state 与 创建的 DOM 都会被保留)。它可以帮助你找出需要添加清理函数的 Effect,并在早期暴露类似竞态条件这样的 bug。此外,每当你在开发环境中保存文件时,React 也会重新挂载 Effect。这些行为都仅限于开发环境。
摘要
- Unlike events, Effects are caused by rendering itself rather than a particular interaction.
- 与事件不同,Effect 由渲染本身引起,而非特定的交互。
- Effects let you synchronize a component with some external system (third-party API, network, etc).
- Effect 允许你将组件与某些外部系统(第三方 API、网络等)同步。
- By default, Effects run after every render (including the initial one).
- 默认情况下,Effect 在每次渲染(包括初始渲染)后运行。
- React will skip the Effect if all of its dependencies have the same values as during the last render.
- 如果所有依赖项都与上一次渲染时相同,React 会跳过本次 Effect。
- You can’t “choose” your dependencies. They are determined by the code inside the Effect.
- 你不能“选择”依赖项,它们是由 Effect 内部的代码所决定的。
- Empty dependency array (
[]
) corresponds to the component “mounting”, i.e. being added to the screen. - 空的依赖数组(
[]
)对应于组件的“挂载”,即组件被添加到页面上时。 - In Strict Mode, React mounts components twice (in development only!) to stress-test your Effects.
- 仅在严格模式下的开发环境中,React 会挂载两次组件,以对 Effect 进行压力测试。
- If your Effect breaks because of remounting, you need to implement a cleanup function.
- 如果你的 Effect 因为重新挂载而出现问题,那么你需要实现一个清理函数。
- React will call your cleanup function before the Effect runs next time, and during the unmount.
- React 会在 Effect 再次运行之前和在组件卸载时调用你的清理函数。
第 1 个挑战 共 4 个挑战: 挂载后聚焦于表单字段 | Focus a field on mount
In this example, the form renders a <MyInput />
component.
在下面的例子中,表单中渲染了一个 <MyInput />
组件。
Use the input’s focus()
method to make MyInput
automatically focus when it appears on the screen. There is already a commented out implementation, but it doesn’t quite work. Figure out why it doesn’t work, and fix it. (If you’re familiar with the autoFocus
attribute, pretend that it does not exist: we are reimplementing the same functionality from scratch.)
使用输入框的 focus()
方法,让 MyInput
在页面上出现时自动聚焦。已经有一个被注释掉的实现,但它并不能正常工作。找出它为什么不起作用,并修复它。如果你熟悉 autoFocus
属性,请假装它不存在:我们正在从头开始实现相同的功能。
import { useEffect, useRef } from 'react'; export default function MyInput({ value, onChange }) { const ref = useRef(null); // TODO: This doesn't quite work. Fix it. // ref.current.focus() // TODO:下面的这种做法不会生效,请修复。 // ref.current.focus() return ( <input ref={ref} value={value} onChange={onChange} /> ); }
To verify that your solution works, press “Show form” and verify that the input receives focus (becomes highlighted and the cursor is placed inside). Press “Hide form” and “Show form” again. Verify the input is highlighted again. 要验证你的方法是否奏效,请点击“展示表单”,并确认输入框获得焦点(高亮显示并且光标位于内部);再次点击“隐藏表单”和“展示表单”,确认输入框是否再次被高亮显示。
MyInput
should only focus on mount rather than after every render. To verify that the behavior is right, press “Show form” and then repeatedly press the “Make it uppercase” checkbox. Clicking the checkbox should not focus the input above it.
MyInput
组件应该只在 挂载 时获得焦点,而非每次渲染后。为了验证这一行为是否正确,点击“展示表单”,然后反复点击“大写”的复选框。点击复选框时,上方的输入框不应该获得焦点。