diff --git a/src/content/reference/react/useEffect.md b/src/content/reference/react/useEffect.md index 8d04f205c5..4138c336d1 100644 --- a/src/content/reference/react/useEffect.md +++ b/src/content/reference/react/useEffect.md @@ -4,7 +4,7 @@ title: useEffect -`useEffect` is a React Hook that lets you [synchronize a component with an external system.](/learn/synchronizing-with-effects) +`useEffect` 是一个 React Hook,它允许你 [将组件与外部系统同步](/learn/synchronizing-with-effects)。 ```js useEffect(setup, dependencies?) @@ -16,11 +16,11 @@ useEffect(setup, dependencies?) --- -## Reference {/*reference*/} +## 参考 {/*reference*/} ### `useEffect(setup, dependencies?)` {/*useeffect*/} -Call `useEffect` at the top level of your component to declare an Effect: +在组件的顶层调用 `useEffect` 来声明一个 Effect: ```js import { useEffect } from 'react'; @@ -40,43 +40,43 @@ function ChatRoom({ roomId }) { } ``` -[See more examples below.](#usage) +[请看下面的更多例子](#usage)。 -#### Parameters {/*parameters*/} +#### 参数 {/*parameters*/} -* `setup`: The function with your Effect's logic. Your setup function may also optionally return a *cleanup* function. When your component is added to the DOM, React will run your setup function. After every re-render with changed dependencies, React will first run the cleanup function (if you provided it) with the old values, and then run your setup function with the new values. After your component is removed from the DOM, React will run your cleanup function. +* `setup`:处理 Effect 的函数。setup 函数选择性返回一个 **清理(cleanup)** 函数。在将组件首次添加到 DOM 之前,React 将运行 setup 函数。在每次依赖项变更重新渲染后,React 将首先使用旧值运行 cleanup 函数(如果你提供了该函数),然后使用新值运行 setup 函数。在组件从 DOM 中移除后,React 将最后一次运行 cleanup 函数。 -* **optional** `dependencies`: The list of all reactive values referenced inside of the `setup` code. Reactive values include props, state, and all the variables and functions declared directly inside your component body. If your linter is [configured for React](/learn/editor-setup#linting), it will verify that every reactive value is correctly specified as a dependency. The list of dependencies must have a constant number of items and be written inline like `[dep1, dep2, dep3]`. React will compare each dependency with its previous value using the [`Object.is`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is) comparison. If you omit this argument, your Effect will re-run after every re-render of the component. [See the difference between passing an array of dependencies, an empty array, and no dependencies at all.](#examples-dependencies) +* **可选** `dependencies`:`setup` 代码中引用的所有响应式值的列表。响应式值包括 props、state 以及所有直接在组件内部声明的变量和函数。如果你的代码检查工具 [配置了 React](/learn/editor-setup#linting),那么它将验证是否每个响应式值都被正确地指定为一个依赖项。依赖项列表的元素数量必须是固定的,并且必须像 `[dep1, dep2, dep3]` 这样内联编写。React 将使用 [`Object.is`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is) 来比较每个依赖项和它先前的值。如果省略此参数,则在每次重新渲染组件之后,将重新运行 Effect 函数。如果你想了解更多,请参见 [传递依赖数组、空数组和不传递依赖项之间的区别](#examples-dependencies)。 -#### Returns {/*returns*/} +#### 返回值 {/*returns*/} -`useEffect` returns `undefined`. +`useEffect` 返回 `undefined`。 -#### Caveats {/*caveats*/} +#### 注意事项 {/*caveats*/} -* `useEffect` is a Hook, so you can only call it **at the top level of your component** or your own Hooks. You can't call it inside loops or conditions. If you need that, extract a new component and move the state into it. +* `useEffect` 是一个 Hook,因此只能在 **组件的顶层** 或自己的 Hook 中调用它,而不能在循环或者条件内部调用。如果需要,抽离出一个新组件并将 state 移入其中。 -* If you're **not trying to synchronize with some external system,** [you probably don't need an Effect.](/learn/you-might-not-need-an-effect) +* 如果你 **没有打算与某个外部系统同步**,[那么你可能不需要 Effect](/learn/you-might-not-need-an-effect)。 -* When Strict Mode is on, React will **run one extra development-only setup+cleanup cycle** before the first real setup. This is a stress-test that ensures that your cleanup logic "mirrors" your setup logic and that it stops or undoes whatever the setup is doing. If this causes a problem, [implement the cleanup function.](/learn/synchronizing-with-effects#how-to-handle-the-effect-firing-twice-in-development) +* 当严格模式启动时,React 将在真正的 setup 函数首次运行前,**运行一个开发模式下专有的额外 setup + cleanup 周期**。这是一个压力测试,用于确保 cleanup 逻辑“映射”到了 setup 逻辑,并停止或撤消 setup 函数正在做的任何事情。如果这会导致一些问题,[请实现 cleanup 函数](/learn/synchronizing-with-effects#how-to-handle-the-effect-firing-twice-in-development)。 -* If some of your dependencies are objects or functions defined inside the component, there is a risk that they will **cause the Effect to re-run more often than needed.** To fix this, remove unnecessary [object](#removing-unnecessary-object-dependencies) and [function](#removing-unnecessary-function-dependencies) dependencies. You can also [extract state updates](#updating-state-based-on-previous-state-from-an-effect) and [non-reactive logic](#reading-the-latest-props-and-state-from-an-effect) outside of your Effect. +* 如果你的一些依赖项是组件内部定义的对象或函数,则存在这样的风险,即它们将 **导致 Effect 过多地重新运行**。要解决这个问题,请删除不必要的 [对象](/reference/react/useEffect#removing-unnecessary-object-dependencies) 和 [函数](/reference/react/useEffect#removing-unnecessary-function-dependencies) 依赖项。你还可以 [抽离状态更新](/reference/react/useEffect#updating-state-based-on-previous-state-from-an-effect) 和 [非响应式的逻辑](/reference/react/useEffect#reading-the-latest-props-and-state-from-an-effect) 到 Effect 之外。 -* If your Effect wasn't caused by an interaction (like a click), React will let the browser **paint the updated screen first before running your Effect.** If your Effect is doing something visual (for example, positioning a tooltip), and the delay is noticeable (for example, it flickers), replace `useEffect` with [`useLayoutEffect`.](/reference/react/useLayoutEffect) +* 如果你的 Effect 不是由交互(比如点击)引起的,那么 React 会让浏览器 **在运行 Effect 前先绘制出更新后的屏幕**。如果你的 Effect 正在做一些视觉相关的事情(例如,定位一个 tooltip),并且有显著的延迟(例如,它会闪烁),那么将 `useEffect` 替换为 [`useLayoutEffect`](/reference/react/useLayoutEffect)。 -* Even if your Effect was caused by an interaction (like a click), **the browser may repaint the screen before processing the state updates inside your Effect.** Usually, that's what you want. However, if you must block the browser from repainting the screen, you need to replace `useEffect` with [`useLayoutEffect`.](/reference/react/useLayoutEffect) +* 即使你的 Effect 是由一个交互(比如点击)引起的,**浏览器也可能在处理 Effect 内部的状态更新之前重新绘制屏幕**。通常,这就是你想要的。但是,如果你一定要阻止浏览器重新绘制屏幕,则需要用 [`useLayoutEffect`](/reference/react/useLayoutEffect) 替换 `useEffect`。 -* Effects **only run on the client.** They don't run during server rendering. +* Effect **只在客户端上运行**,在服务端渲染中不会运行。 --- -## Usage {/*usage*/} +## 用法 {/*usage*/} -### Connecting to an external system {/*connecting-to-an-external-system*/} +### 连接到外部系统 {/*connecting-to-an-external-system*/} -Some components need to stay connected to the network, some browser API, or a third-party library, while they are displayed on the page. These systems aren't controlled by React, so they are called *external.* +有些组件需要与网络、某些浏览器 API 或第三方库保持连接,当它们显示在页面上时。这些系统不受 React 控制,所以称为外部系统。 -To [connect your component to some external system,](/learn/synchronizing-with-effects) call `useEffect` at the top level of your component: +要 [将组件连接到某个外部系统](/learn/synchronizing-with-effects),请在组件的顶层调用 `useEffect`: ```js [[1, 8, "const connection = createConnection(serverUrl, roomId);"], [1, 9, "connection.connect();"], [2, 11, "connection.disconnect();"], [3, 13, "[serverUrl, roomId]"]] import { useEffect } from 'react'; @@ -96,45 +96,45 @@ function ChatRoom({ roomId }) { } ``` -You need to pass two arguments to `useEffect`: +你需要向 `useEffect` 传递两个参数: -1. A *setup function* with setup code that connects to that system. - - It should return a *cleanup function* with cleanup code that disconnects from that system. -2. A list of dependencies including every value from your component used inside of those functions. +1. 一个 **setup 函数** ,其 setup 代码 用来连接到该系统。 + - 它应该返回一个 **清理函数**(cleanup),其 cleanup 代码 用来与该系统断开连接。 +2. 一个 依赖项列表,包括这些函数使用的每个组件内的值。 -**React calls your setup and cleanup functions whenever it's necessary, which may happen multiple times:** +**React 在必要时会调用 setup 和 cleanup,这可能会发生多次**: -1. Your setup code runs when your component is added to the page *(mounts)*. -2. After every re-render of your component where the dependencies have changed: - - First, your cleanup code runs with the old props and state. - - Then, your setup code runs with the new props and state. -3. Your cleanup code runs one final time after your component is removed from the page *(unmounts).* +1. 将组件挂载到页面时,将运行 setup 代码。 +2. 重新渲染 依赖项 变更的组件后: + - 首先,使用旧的 props 和 state 运行 cleanup 代码。 + - 然后,使用新的 props 和 state 运行 setup 代码。 +3. 当组件从页面卸载后,cleanup 代码 将运行最后一次。 -**Let's illustrate this sequence for the example above.** +**用上面的代码作为例子来解释这个顺序**。 -When the `ChatRoom` component above gets added to the page, it will connect to the chat room with the initial `serverUrl` and `roomId`. If either `serverUrl` or `roomId` change as a result of a re-render (say, if the user picks a different chat room in a dropdown), your Effect will *disconnect from the previous room, and connect to the next one.* When the `ChatRoom` component is removed from the page, your Effect will disconnect one last time. +当 `ChatRoom` 组件添加到页面中时,它将使用 `serverUrl` 和 `roomId` 初始值连接到聊天室。如果 `serverUrl` 或者 `roomId` 发生改变并导致重新渲染(比如用户在下拉列表中选择了一个不同的聊天室),那么 Effect 就会 **断开与前一个聊天室的连接,并连接到下一个聊天室**。当 `ChatRoom` 组件从页面中卸载时,你的 Effect 将最后一次断开连接。 -**To [help you find bugs,](/learn/synchronizing-with-effects#step-3-add-cleanup-if-needed) in development React runs setup and cleanup one extra time before the setup.** This is a stress-test that verifies your Effect's logic is implemented correctly. If this causes visible issues, your cleanup function is missing some logic. The cleanup function should stop or undo whatever the setup function was doing. The rule of thumb is that the user shouldn't be able to distinguish between the setup being called once (as in production) and a *setup* → *cleanup* → *setup* sequence (as in development). [See common solutions.](/learn/synchronizing-with-effects#how-to-handle-the-effect-firing-twice-in-development) +**为了 [帮助你发现 bug](/learn/synchronizing-with-effects#step-3-add-cleanup-if-needed),在开发环境下,React 在运行 setup 之前会额外运行一次setupcleanup**。这是一个压力测试,用于验证 Effect 逻辑是否正确实现。如果这会导致可见的问题,那么你的 cleanup 函数就缺少一些逻辑。cleanup 函数应该停止或撤消 setup 函数正在执行的任何操作。一般来说,用户不应该能够区分只调用一次 setup(在生产环境中)与调用 *setup* → *cleanup* → *setup* 序列(在开发环境中)。[查看常见解决方案](/learn/synchronizing-with-effects#how-to-handle-the-effect-firing-twice-in-development)。 -**Try to [write every Effect as an independent process](/learn/lifecycle-of-reactive-effects#each-effect-represents-a-separate-synchronization-process) and [think about a single setup/cleanup cycle at a time.](/learn/lifecycle-of-reactive-effects#thinking-from-the-effects-perspective)** It shouldn't matter whether your component is mounting, updating, or unmounting. When your cleanup logic correctly "mirrors" the setup logic, your Effect is resilient to running setup and cleanup as often as needed. +**尽量 [将每个 Effect 作为一个独立的过程编写](/learn/lifecycle-of-reactive-effects#each-effect-represents-a-separate-synchronization-process),并且 [每次只考虑一个单独的 setup/cleanup 周期](/learn/lifecycle-of-reactive-effects#thinking-from-the-effects-perspective)**。组件是否正在挂载、更新或卸载并不重要。当你的 cleanup 逻辑正确地“映射”到 setup 逻辑时,你的 Effect 是可复原的,因此可以根据需要多次运行 setup 和 cleanup 函数。 -An Effect lets you [keep your component synchronized](/learn/synchronizing-with-effects) with some external system (like a chat service). Here, *external system* means any piece of code that's not controlled by React, such as: +Effect 可以让你的组件与某些外部系统(比如聊天服务)[保持同步](/learn/synchronizing-with-effects)。在这里,外部系统是指任何不受 React 控制的代码,例如: -* A timer managed with [`setInterval()`](https://developer.mozilla.org/en-US/docs/Web/API/setInterval) and [`clearInterval()`](https://developer.mozilla.org/en-US/docs/Web/API/clearInterval). -* An event subscription using [`window.addEventListener()`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener) and [`window.removeEventListener()`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener). -* A third-party animation library with an API like `animation.start()` and `animation.reset()`. +* 由 [`setInterval()`](https://developer.mozilla.org/en-US/docs/Web/API/setInterval)[`clearInterval()`](https://developer.mozilla.org/en-US/docs/Web/API/clearInterval) 管理的定时器。 +* 使用 [`window.addEventListener()`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener)[`window.removeEventListener()`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener) 的事件订阅。 +* 一个第三方的动画库,它有一个类似 `animation.start()``animation.reset()` 的 API。 -**If you're not connecting to any external system, [you probably don't need an Effect.](/learn/you-might-not-need-an-effect)** +**如果你没有连接到任何外部系统,[你或许不需要 Effect](/learn/you-might-not-need-an-effect)**。 - + -#### Connecting to a chat server {/*connecting-to-a-chat-server*/} +#### 连接到聊天服务器 {/*connecting-to-a-chat-server*/} -In this example, the `ChatRoom` component uses an Effect to stay connected to an external system defined in `chat.js`. Press "Open chat" to make the `ChatRoom` component appear. This sandbox runs in development mode, so there is an extra connect-and-disconnect cycle, as [explained here.](/learn/synchronizing-with-effects#step-3-add-cleanup-if-needed) Try changing the `roomId` and `serverUrl` using the dropdown and the input, and see how the Effect re-connects to the chat. Press "Close chat" to see the Effect disconnect one last time. +在这个示例中,`ChatRoom` 组件使用一个 Effect 来保持与 `chat.js` 中定义的外部系统的连接。点击“打开聊天”以显示 `ChatRoom` 组件。这个沙盒在开发模式下运行,因此有一个额外的“连接并断开”的周期,就像 [这里描述的](/learn/synchronizing-with-effects#step-3-add-cleanup-if-needed) 一样。尝试使用下拉菜单和输入框更改 `roomId` 和 `serverUrl`,并查看 Effect 如何重新连接到聊天。点击“关闭聊天”可以看到 Effect 最后一次断开连接。 @@ -195,7 +195,7 @@ export default function App() { ```js chat.js export function createConnection(serverUrl, roomId) { - // A real implementation would actually connect to the server + // 真正的实现会实际连接到服务器 return { connect() { console.log('✅ Connecting to "' + roomId + '" room at ' + serverUrl + '...'); @@ -216,9 +216,9 @@ button { margin-left: 10px; } -#### Listening to a global browser event {/*listening-to-a-global-browser-event*/} +#### 监听全局的浏览器事件 {/*listening-to-a-global-browser-event*/} -In this example, the external system is the browser DOM itself. Normally, you'd specify event listeners with JSX, but you can't listen to the global [`window`](https://developer.mozilla.org/en-US/docs/Web/API/Window) object this way. An Effect lets you connect to the `window` object and listen to its events. Listening to the `pointermove` event lets you track the cursor (or finger) position and update the red dot to move with it. +在这个例子中,外部系统就是浏览器 DOM 本身。通常,你会使用 JSX 指定事件监听,但是你不能以这种方式监听全局的 [`window`](https://developer.mozilla.org/en-US/docs/Web/API/Window) 对象。你可以通过 Effect 连接到 `window` 对象并监听其事件。如监听 `pointermove` 事件可以让你追踪光标(或手指)的位置,并更新红点以随之移动。 @@ -265,9 +265,9 @@ body { -#### Triggering an animation {/*triggering-an-animation*/} +#### 触发动画效果 {/*triggering-an-animation*/} -In this example, the external system is the animation library in `animation.js`. It provides a JavaScript class called `FadeInAnimation` that takes a DOM node as an argument and exposes `start()` and `stop()` methods to control the animation. This component [uses a ref](/learn/manipulating-the-dom-with-refs) to access the underlying DOM node. The Effect reads the DOM node from the ref and automatically starts the animation for that node when the component appears. +在这个例子中,外部系统是 `animation.js` 中的动画库。它提供了一个名为 `FadeInAnimation` 的 JavaScript 类,该类接受一个 DOM 节点作为参数,并暴露 `start()` 和 `stop()` 方法来控制动画。此组件 [使用 ref](/learn/manipulating-the-dom-with-refs) 访问底层 DOM 节点。Effect 从 ref 中读取 DOM 节点,并在组件出现时自动开启该节点的动画。 @@ -325,11 +325,11 @@ export class FadeInAnimation { start(duration) { this.duration = duration; if (this.duration === 0) { - // Jump to end immediately + // 立刻跳转到最后 this.onProgress(1); } else { this.onProgress(0); - // Start animating + // 开始动画 this.startTime = performance.now(); this.frameId = requestAnimationFrame(() => this.onFrame()); } @@ -339,7 +339,7 @@ export class FadeInAnimation { const progress = Math.min(timePassed / this.duration, 1); this.onProgress(progress); if (progress < 1) { - // We still have more frames to paint + // 仍然有更多的帧要绘制 this.frameId = requestAnimationFrame(() => this.onFrame()); } } @@ -364,9 +364,9 @@ html, body { min-height: 300px; } -#### Controlling a modal dialog {/*controlling-a-modal-dialog*/} +#### 控制模态对话框 {/*controlling-a-modal-dialog*/} -In this example, the external system is the browser DOM. The `ModalDialog` component renders a [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog) element. It uses an Effect to synchronize the `isOpen` prop to the [`showModal()`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/showModal) and [`close()`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/close) method calls. +在这个例子中,外部系统是浏览器 DOM。`ModalDialog` 组件渲染一个 [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog) 元素。它使用 Effect 将 `isOpen` prop 同步到 [`showModal()`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/showModal) 和 [`close()`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/close) 方法调用。 @@ -424,9 +424,9 @@ body { -#### Tracking element visibility {/*tracking-element-visibility*/} +#### 跟踪元素可见性 {/*tracking-element-visibility*/} -In this example, the external system is again the browser DOM. The `App` component displays a long list, then a `Box` component, and then another long list. Scroll the list down. Notice that when the `Box` component appears in the viewport, the background color changes to black. To implement this, the `Box` component uses an Effect to manage an [`IntersectionObserver`](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API). This browser API notifies you when the DOM element is visible in the viewport. +在这个例子中,外部系统仍然是浏览器 DOM。`App` 组件展示一个长列表,然后是 `Box` 组件,然后是另一个长列表。试试向下滚动列表。请注意,当 `Box` 组件出现在视野中时,背景色会变成黑色。为了实现这一点,`Box` 组件使用 Effect 来管理 [`IntersectionObserver`](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API)。这个浏览器 API 会在视野中出现指定 DOM 元素时通知你。 @@ -500,11 +500,11 @@ export default function Box() { --- -### Wrapping Effects in custom Hooks {/*wrapping-effects-in-custom-hooks*/} +### 在自定义 Hook 中封装 Effect {/*wrapping-effects-in-custom-hooks*/} -Effects are an ["escape hatch":](/learn/escape-hatches) you use them when you need to "step outside React" and when there is no better built-in solution for your use case. If you find yourself often needing to manually write Effects, it's usually a sign that you need to extract some [custom Hooks](/learn/reusing-logic-with-custom-hooks) for common behaviors your components rely on. +Effect 是一个 [“逃生出口”](/learn/escape-hatches):当你需要“走出 React 之外”或者当你的使用场景没有更好的内置解决方案时,你可以使用它们。如果你发现自己经常需要手动编写 Effect,那么这通常表明你需要为组件所依赖的通用行为提取一些 [自定义 Hook](/learn/reusing-logic-with-custom-hooks)。 -For example, this `useChatRoom` custom Hook "hides" the logic of your Effect behind a more declarative API: +例如,这个 `useChatRoom` 自定义 Hook 把 Effect 的逻辑“隐藏”在一个更具声明性的 API 之后: ```js {1,11} function useChatRoom({ serverUrl, roomId }) { @@ -520,7 +520,7 @@ function useChatRoom({ serverUrl, roomId }) { } ``` -Then you can use it from any component like this: +然后你可以像这样从任何组件使用它: ```js {4-7} function ChatRoom({ roomId }) { @@ -533,15 +533,15 @@ function ChatRoom({ roomId }) { // ... ``` -There are also many excellent custom Hooks for every purpose available in the React ecosystem. +在 React 生态系统中,还有许多用于各种用途的优秀的自定义 Hook。 -[Learn more about wrapping Effects in custom Hooks.](/learn/reusing-logic-with-custom-hooks) +[了解有关在自定义 Hook 中封装 Effect 的更多信息](/learn/reusing-logic-with-custom-hooks)。 - + -#### Custom `useChatRoom` Hook {/*custom-usechatroom-hook*/} +#### 定制 `useChatRoom` Hook {/*custom-usechatroom-hook*/} -This example is identical to one of the [earlier examples,](#examples-connecting) but the logic is extracted to a custom Hook. +此示例与 [前面的一个示例](#examples-connecting) 相同,但是逻辑被提取到一个自定义 Hook 中。 @@ -614,7 +614,7 @@ export function useChatRoom({ serverUrl, roomId }) { ```js chat.js export function createConnection(serverUrl, roomId) { - // A real implementation would actually connect to the server + // 真正的实现将实际连接到服务器 return { connect() { console.log('✅ Connecting to "' + roomId + '" room at ' + serverUrl + '...'); @@ -635,9 +635,9 @@ button { margin-left: 10px; } -#### Custom `useWindowListener` Hook {/*custom-usewindowlistener-hook*/} +#### 定制 `useWindowListener` Hook {/*custom-usewindowlistener-hook*/} -This example is identical to one of the [earlier examples,](#examples-connecting) but the logic is extracted to a custom Hook. +此示例与 [前面的一个示例](#examples-connecting) 相同,但是逻辑被提取到一个自定义 Hook 中。 @@ -692,9 +692,9 @@ body { -#### Custom `useIntersectionObserver` Hook {/*custom-useintersectionobserver-hook*/} +#### 定制 `useIntersectionObserver` Hook {/*custom-useintersectionobserver-hook*/} -This example is identical to one of the [earlier examples,](#examples-connecting) but the logic is partially extracted to a custom Hook. +此示例与 [前面的一个示例](#examples-connecting) 相同,但是部分逻辑被提取到自定义 Hook 中。 @@ -784,11 +784,11 @@ export function useIntersectionObserver(ref) { --- -### Controlling a non-React widget {/*controlling-a-non-react-widget*/} +### 控制非 React 小部件 {/*controlling-a-non-react-widget*/} -Sometimes, you want to keep an external system synchronized to some prop or state of your component. +有时,你希望外部系统与你组件的某些 props 或 state 保持同步。 -For example, if you have a third-party map widget or a video player component written without React, you can use an Effect to call methods on it that make its state match the current state of your React component. This Effect creates an instance of a `MapWidget` class defined in `map-widget.js`. When you change the `zoomLevel` prop of the `Map` component, the Effect calls the `setZoom()` on the class instance to keep it synchronized: +例如,如果你有一个没有使用 React 编写的第三方地图小部件或视频播放器组件,你可以使用 Effect 调用该组件上的方法,使其状态与 React 组件的当前状态相匹配。此 Effect 创建了在 `map-widget.js` 中定义的 `MapWidget` 类的实例。当你更改 `Map` 组件的 `zoomLevel` prop 时,Effect 调用类实例上的 `setZoom()` 来保持同步: @@ -888,15 +888,15 @@ button { margin: 5px; } -In this example, a cleanup function is not needed because the `MapWidget` class manages only the DOM node that was passed to it. After the `Map` React component is removed from the tree, both the DOM node and the `MapWidget` class instance will be automatically garbage-collected by the browser JavaScript engine. +在本例中,不需要 cleanup 函数,因为 `MapWidget` 类只管理传递给它的 DOM 节点。从树中删除 `Map` React 组件后,DOM 节点和 `MapWidget` 类实例都将被浏览器的 JavaScript 引擎的垃圾回收机制自动处理掉。 --- -### Fetching data with Effects {/*fetching-data-with-effects*/} +### 使用 Effect 请求数据 {/*fetching-data-with-effects*/} -You can use an Effect to fetch data for your component. Note that [if you use a framework,](/learn/start-a-new-react-project#production-grade-react-frameworks) using your framework's data fetching mechanism will be a lot more efficient than writing Effects manually. +你可以使用 Effect 来为组件请求数据。请注意,[如果你使用框架](/learn/start-a-new-react-project#production-grade-react-frameworks),使用框架的数据请求方式将比在 Effect 中手动编写要有效得多。 -If you want to fetch data from an Effect manually, your code might look like this: +如果你想手动从 Effect 中请求数据,你的代码可能是这样的: ```js import { useState, useEffect } from 'react'; @@ -922,7 +922,7 @@ export default function Page() { // ... ``` -Note the `ignore` variable which is initialized to `false`, and is set to `true` during cleanup. This ensures [your code doesn't suffer from "race conditions":](https://maxrozen.com/race-conditions-fetching-data-react-with-useeffect) network responses may arrive in a different order than you sent them. +注意,`ignore` 变量被初始化为 `false`,并且在 cleanup 中被设置为 `true`。这样可以确保 [你的代码不会受到“竞争条件”的影响](https://maxrozen.com/race-conditions-fetching-data-react-with-useeffect):网络响应可能会以与你发送的不同的顺序到达。 @@ -975,7 +975,7 @@ export async function fetchBio(person) { -You can also rewrite using the [`async` / `await`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function) syntax, but you still need to provide a cleanup function: +你也可以使用 [`async` / `await`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function) 语法重写,但是你仍然需要提供一个 cleanup 函数: @@ -1031,50 +1031,50 @@ export async function fetchBio(person) { -Writing data fetching directly in Effects gets repetitive and makes it difficult to add optimizations like caching and server rendering later. [It's easier to use a custom Hook--either your own or maintained by the community.](/learn/reusing-logic-with-custom-hooks#when-to-use-custom-hooks) +直接在 Effect 中编写数据请求会显得重复,并且很难在以后添加缓存和服务端渲染等优化。[使用自定义 Hook 更简单——不管是你自己的 Hook 还是由社区维护的 Hook](/learn/reusing-logic-with-custom-hooks#when-to-use-custom-hooks)。 -#### What are good alternatives to data fetching in Effects? {/*what-are-good-alternatives-to-data-fetching-in-effects*/} +#### Effect 中的数据请求有什么好的替代方法 {/*what-are-good-alternatives-to-data-fetching-in-effects*/} -Writing `fetch` calls inside Effects is a [popular way to fetch data](https://www.robinwieruch.de/react-hooks-fetch-data/), especially in fully client-side apps. This is, however, a very manual approach and it has significant downsides: +在 Effect 中使用 `fetch` 是 [请求数据的一种流行方式](https://www.robinwieruch.de/react-hooks-fetch-data/),特别是在完全的客户端应用程序中。然而,这是一种非常手动的方法,而且有很大的缺点: -- **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. -- **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. -- **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. -- **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.](https://maxrozen.com/race-conditions-fetching-data-react-with-useeffect) +- **Effect 不在服务器上运行**。这意味着初始服务器渲染的 HTML 将只包含没有数据的 loading 状态。客户端电脑仅为了发现它现在需要加载数据,将不得不下载所有的脚本来渲染你的应用程序。这并不高效。 +- **在 Effect 中直接请求数据很容易导致“网络瀑布”**。当你渲染父组件时,它会请求一些数据,再渲染子组件,然后重复这样的过程来请求子组件的数据。如果网络不是很快,这将比并行请求所有数据要慢得多。 +- **在 Effect 中直接请求数据通常意味着你不会预加载或缓存数据**。例如,如果组件卸载后重新挂载,它不得不再次请求数据。 +- **这不符合工效学**。在调用 `fetch` 时,需要编写大量样板代码,以避免像 [竞争条件](https://maxrozen.com/race-conditions-fetching-data-react-with-useeffect) 这样的 bug。 -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](/learn/start-a-new-react-project#production-grade-react-frameworks), 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. -- **Otherwise, consider using or building a client-side cache.** Popular open source solutions include [React Query](https://react-query.tanstack.com/), [useSWR](https://swr.vercel.app/), and [React Router 6.4+.](https://beta.reactrouter.com/en/main/start/overview) You can build your own solution too, in which case you would use Effects under the hood but also add logic for deduplicating requests, caching responses, and avoiding network waterfalls (by preloading data or hoisting data requirements to routes). +- **如果使用 [框架](/learn/start-a-new-react-project#production-grade-react-frameworks),请使用其内置的数据请求机制**。现代的 React 框架集成了高效的数据请求机制,不会受到上述问题的影响。 +否则,请考虑使用或构建客户端缓存。流行的开源解决方案包括 [React Query](https://react-query.tanstack.com/)、[useSWR](https://swr.vercel.app/) 和 [React Router v6.4+](https://beta.reactrouter.com/en/main/start/overview)。你也可以构建自己的解决方案,在这种情况下,你可以在掌控下使用 Effect,但也要添加逻辑来处理重复的请求、缓存响应和避免“网络瀑布”(通过预加载数据或将数据需求提升到路由层面)。 -You can continue fetching data directly in Effects if neither of these approaches suit you. +如果这两种方法都不适合你,你可以继续直接在 Effect 中请求数据。 --- -### Specifying reactive dependencies {/*specifying-reactive-dependencies*/} +### 指定响应式依赖项 {/*specifying-reactive-dependencies*/} -**Notice that you can't "choose" the dependencies of your Effect.** Every reactive value used by your Effect's code must be declared as a dependency. Your Effect's dependency list is determined by the surrounding code: +**注意,你无法“选择” Effect 的依赖项**。Effect 代码中使用的每个 响应式值 都必须声明为依赖项。你的 Effect 依赖列表是由周围代码决定的: ```js [[2, 1, "roomId"], [2, 2, "serverUrl"], [2, 5, "serverUrl"], [2, 5, "roomId"], [2, 8, "serverUrl"], [2, 8, "roomId"]] -function ChatRoom({ roomId }) { // This is a reactive value - const [serverUrl, setServerUrl] = useState('https://localhost:1234'); // This is a reactive value too +function ChatRoom({ roomId }) { // 这是一个响应式值 + const [serverUrl, setServerUrl] = useState('https://localhost:1234'); // 这也是一个响应式值 useEffect(() => { - const connection = createConnection(serverUrl, roomId); // This Effect reads these reactive values + const connection = createConnection(serverUrl, roomId); // 这个 Effect 读取这些响应式值 connection.connect(); return () => connection.disconnect(); - }, [serverUrl, roomId]); // ✅ So you must specify them as dependencies of your Effect + }, [serverUrl, roomId]); // ✅ 因此你必须指定它们作为 Effect 的依赖项 // ... } ``` -If either `serverUrl` or `roomId` change, your Effect will reconnect to the chat using the new values. +如果 `serverUrl` 或 `roomId` 任意一个发生变化,那么 Effect 将使用新的值重新连接到聊天室。 -**[Reactive values](/learn/lifecycle-of-reactive-effects#effects-react-to-reactive-values) include props and all variables and functions declared directly inside of your component.** Since `roomId` and `serverUrl` are reactive values, you can't remove them from the dependencies. If you try to omit them and [your linter is correctly configured for React,](/learn/editor-setup#linting) the linter will flag this as a mistake you need to fix: +**[响应式值](/learn/lifecycle-of-reactive-effects#effects-react-to-reactive-values) 包括 props 和直接在组件内声明的所有变量和函数**。由于 `roomId` 和 `serverUrl` 是响应式值,你不能将它们从依赖项中移除。如果你试图省略它们,并且你的代码检查工具针对 React 进行了正确的配置,那么代码检查工具会将它标记为需要修复的错误: ```js {8} function ChatRoom({ roomId }) { @@ -1084,73 +1084,73 @@ function ChatRoom({ roomId }) { const connection = createConnection(serverUrl, roomId); connection.connect(); return () => connection.disconnect(); - }, []); // 🔴 React Hook useEffect has missing dependencies: 'roomId' and 'serverUrl' + }, []); // 🔴 React Hook useEffect 缺少依赖项:'roomId' 和 'serverUrl' // ... } ``` -**To remove a dependency, you need to ["prove" to the linter that it *doesn't need* to be a dependency.](/learn/removing-effect-dependencies#removing-unnecessary-dependencies)** For example, you can move `serverUrl` out of your component to prove that it's not reactive and won't change on re-renders: +**要删除一个依赖项,你需要 [“证明”给代码检查工具,表明它 **不需要** 作为一个依赖项](/learn/removing-effect-dependencies#removing-unnecessary-dependencies)**。例如,你可以将 `serverUrl` 声明移动到组件外面,以证明它不是响应式的,并且不会在重新渲染时发生变化: ```js {1,8} -const serverUrl = 'https://localhost:1234'; // Not a reactive value anymore +const serverUrl = 'https://localhost:1234'; // 不再是一个响应式值 function ChatRoom({ roomId }) { useEffect(() => { const connection = createConnection(serverUrl, roomId); connection.connect(); return () => connection.disconnect(); - }, [roomId]); // ✅ All dependencies declared + }, [roomId]); // ✅ 所有声明的依赖项 // ... } ``` -Now that `serverUrl` is not a reactive value (and can't change on a re-render), it doesn't need to be a dependency. **If your Effect's code doesn't use any reactive values, its dependency list should be empty (`[]`):** +现在 `serverUrl` 不再是一个响应式值(并且在重新渲染时也不会更改),它就不需要成为一个依赖项。**如果 Effect 的代码不使用任何响应式值,则其依赖项列表应为空(`[]`)**: ```js {1,2,9} -const serverUrl = 'https://localhost:1234'; // Not a reactive value anymore -const roomId = 'music'; // Not a reactive value anymore +const serverUrl = 'https://localhost:1234'; // 不再是响应式值 +const roomId = 'music'; // 不再是响应式值 function ChatRoom() { useEffect(() => { const connection = createConnection(serverUrl, roomId); connection.connect(); return () => connection.disconnect(); - }, []); // ✅ All dependencies declared + }, []); // ✅ 所有声明的依赖项 // ... } ``` -[An Effect with empty dependencies](/learn/lifecycle-of-reactive-effects#what-an-effect-with-empty-dependencies-means) doesn't re-run when any of your component's props or state change. +[依赖项为空数组的 Effect](/learn/lifecycle-of-reactive-effects#what-an-effect-with-empty-dependencies-means) 不会在组件任何的 props 或 state 发生改变时重新运行。 -If you have an existing codebase, you might have some Effects that suppress the linter like this: +如果你有一个现有的代码库,可能会有一些 Effect 像这样抑制了代码检查工具: ```js {3-4} useEffect(() => { // ... - // 🔴 Avoid suppressing the linter like this: + // 🔴 避免这样抑制代码检查工具: // eslint-ignore-next-line react-hooks/exhaustive-deps }, []); ``` -**When dependencies don't match the code, there is a high risk of introducing bugs.** By suppressing the linter, you "lie" to React about the values your Effect depends on. [Instead, prove they're unnecessary.](/learn/removing-effect-dependencies#removing-unnecessary-dependencies) +**当依赖项不匹配代码时,引入 bug 的风险很高**。通过抑制代码检查工具,你“欺骗”了 React 关于你 Effect 所依赖的值。[相反,证明它们是不必要的](/learn/removing-effect-dependencies#removing-unnecessary-dependencies)。 - + -#### Passing a dependency array {/*passing-a-dependency-array*/} +#### 传递依赖项数组 {/*passing-a-dependency-array*/} -If you specify the dependencies, your Effect runs **after the initial render _and_ after re-renders with changed dependencies.** +如果指定了依赖项,则 Effect 在 **初始渲染后以及依赖项变更的重新渲染后** 运行。 ```js {3} useEffect(() => { // ... -}, [a, b]); // Runs again if a or b are different +}, [a, b]); // 如果 a 或 b 不同则会再次运行 ``` -In the below example, `serverUrl` and `roomId` are [reactive values,](/learn/lifecycle-of-reactive-effects#effects-react-to-reactive-values) so they both must be specified as dependencies. As a result, selecting a different room in the dropdown or editing the server URL input causes the chat to re-connect. However, since `message` isn't used in the Effect (and so it isn't a dependency), editing the message doesn't re-connect to the chat. +在下面的示例中,`serverUrl` 和 `roomId` 是 [响应式值](/learn/lifecycle-of-reactive-effects#effects-react-to-reactive-values),所以它们都必须被指定为依赖项。因此,在下拉列表中选择不同的聊天室或编辑服务器 URL 输入框会导致重新连接聊天室。但是,由于 `message` 没有在 Effect 中使用(所以它不是依赖项),编辑消息不会重新连接聊天室。 @@ -1216,7 +1216,7 @@ export default function App() { ```js chat.js export function createConnection(serverUrl, roomId) { - // A real implementation would actually connect to the server + // 真正的实现将实际连接到服务器 return { connect() { console.log('✅ Connecting to "' + roomId + '" room at ' + serverUrl + '...'); @@ -1237,20 +1237,20 @@ button { margin-left: 5px; } -#### Passing an empty dependency array {/*passing-an-empty-dependency-array*/} +#### 传递空依赖项数组 {/*passing-an-empty-dependency-array*/} -If your Effect truly doesn't use any reactive values, it will only run **after the initial render.** +如果你的 Effect 确实没有使用任何响应式值,则它仅在 **初始渲染后** 运行。 ```js {3} useEffect(() => { // ... -}, []); // Does not run again (except once in development) +}, []); // 不会再次运行(开发环境下除外) ``` -**Even with empty dependencies, setup and cleanup will [run one extra time in development](/learn/synchronizing-with-effects#how-to-handle-the-effect-firing-twice-in-development) to help you find bugs.** +**即使依赖项为空,setup 和 cleanup 函数也会 [在开发中额外多运行一次](/learn/synchronizing-with-effects#how-to-handle-the-effect-firing-twice-in-development),以帮你发现 bug**。 -In this example, both `serverUrl` and `roomId` are hardcoded. Since they're declared outside the component, they are not reactive values, and so they aren't dependencies. The dependency list is empty, so the Effect doesn't re-run on re-renders. +在这个示例中,`serverUrl` 和 `roomId` 都是硬编码的。由于它们在组件外部声明,它们不是响应式值,因此它们不是依赖项。依赖项列表为空,因此 Effect 不会在重新渲染时重新运行。 @@ -1297,7 +1297,7 @@ export default function App() { ```js chat.js export function createConnection(serverUrl, roomId) { - // A real implementation would actually connect to the server + // 真正的实现将实际连接到服务器 return { connect() { console.log('✅ Connecting to "' + roomId + '" room at ' + serverUrl + '...'); @@ -1314,17 +1314,17 @@ export function createConnection(serverUrl, roomId) { -#### Passing no dependency array at all {/*passing-no-dependency-array-at-all*/} +#### 不传递依赖项数组 {/*passing-no-dependency-array-at-all*/} -If you pass no dependency array at all, your Effect runs **after every single render (and re-render)** of your component. +如果完全不传递依赖数组,则 Effect 会在组件的 **每次单独渲染(和重新渲染)之后** 运行。 ```js {3} useEffect(() => { // ... -}); // Always runs again +}); // 总是再次运行 ``` -In this example, the Effect re-runs when you change `serverUrl` and `roomId`, which is sensible. However, it *also* re-runs when you change the `message`, which is probably undesirable. This is why usually you'll specify the dependency array. +在本例中,当你更改 `serverUrl` 和 `roomId` 时,Effect 会重新运行,这是合理的。然而,当更改 `message` 时,它也会重新运行,这可能不是希望的。这就是通常要指定依赖项数组的原因。 @@ -1342,7 +1342,7 @@ function ChatRoom({ roomId }) { return () => { connection.disconnect(); }; - }); // No dependency array at all + }); // 没有依赖数组 return ( <> @@ -1390,7 +1390,7 @@ export default function App() { ```js chat.js export function createConnection(serverUrl, roomId) { - // A real implementation would actually connect to the server + // 真正的实现将实际连接到服务器 return { connect() { console.log('✅ Connecting to "' + roomId + '" room at ' + serverUrl + '...'); @@ -1415,9 +1415,9 @@ button { margin-left: 5px; } --- -### Updating state based on previous state from an Effect {/*updating-state-based-on-previous-state-from-an-effect*/} +### 在 Effect 中根据先前 state 更新 state {/*updating-state-based-on-previous-state-from-an-effect*/} -When you want to update state based on previous state from an Effect, you might run into a problem: +当你想要在 Effect 中根据先前的 state 更新 state 时,你可能会遇到问题: ```js {6,9} function Counter() { @@ -1425,17 +1425,17 @@ function Counter() { useEffect(() => { const intervalId = setInterval(() => { - setCount(count + 1); // You want to increment the counter every second... + setCount(count + 1); // 你想要每秒递增该计数器... }, 1000) return () => clearInterval(intervalId); - }, [count]); // 🚩 ... but specifying `count` as a dependency always resets the interval. + }, [count]); // 🚩 ... 但是指定 `count` 作为依赖项总是重置间隔定时器。 // ... } ``` -Since `count` is a reactive value, it must be specified in the list of dependencies. However, that causes the Effect to cleanup and setup again every time the `count` changes. This is not ideal. +因为 `count` 是一个响应式值,所以必须在依赖项列表中指定它。但是,这会导致 Effect 在每次 `count` 更改时再次执行 cleanup 和 setup。这并不理想。 -To fix this, [pass the `c => c + 1` state updater](/reference/react/useState#updating-state-based-on-the-previous-state) to `setCount`: +为了解决这个问题,将 [`c => c + 1` 状态更新器传递给](/reference/react/useState#updating-state-based-on-the-previous-state) `setCount`: @@ -1447,10 +1447,10 @@ export default function Counter() { useEffect(() => { const intervalId = setInterval(() => { - setCount(c => c + 1); // ✅ Pass a state updater + setCount(c => c + 1); // ✅ 传递一个 state 更新器 }, 1000); return () => clearInterval(intervalId); - }, []); // ✅ Now count is not a dependency + }, []); // ✅现在 count 不是一个依赖项 return

{count}

; } @@ -1470,14 +1470,14 @@ body {
-Now that you're passing `c => c + 1` instead of `count + 1`, [your Effect no longer needs to depend on `count`.](/learn/removing-effect-dependencies#are-you-reading-some-state-to-calculate-the-next-state) As a result of this fix, it won't need to cleanup and setup the interval again every time the `count` changes. +现在,你传递的是 `c => c + 1` 而不是 `count + 1`,[因此 Effect 不再需要依赖于 `count`](/learn/removing-effect-dependencies#are-you-reading-some-state-to-calculate-the-next-state)。由于这个修复,每次 `count` 更改时,它都不需要清理(cleanup)和设置(setup)间隔定时器。 --- -### Removing unnecessary object dependencies {/*removing-unnecessary-object-dependencies*/} +### 删除不必要的对象依赖项 {/*removing-unnecessary-object-dependencies*/} -If your Effect depends on an object or a function created during rendering, it might run too often. For example, this Effect re-connects after every render because the `options` object is [different for every render:](/learn/removing-effect-dependencies#does-some-reactive-value-change-unintentionally) +如果你的 Effect 依赖于在渲染期间创建的对象或函数,则它可能会频繁运行。例如,此 Effect 在每次渲染后都重新连接,因为 `options` 对象 [每次渲染都不同](/learn/removing-effect-dependencies#does-some-reactive-value-change-unintentionally): ```js {6-9,12,15} const serverUrl = 'https://localhost:1234'; @@ -1485,20 +1485,20 @@ const serverUrl = 'https://localhost:1234'; function ChatRoom({ roomId }) { const [message, setMessage] = useState(''); - const options = { // 🚩 This object is created from scratch on every re-render + const options = { // 🚩 这个对象在每次渲染时都是从头创建的 serverUrl: serverUrl, roomId: roomId }; useEffect(() => { - const connection = createConnection(options); // It's used inside the Effect + const connection = createConnection(options); // 它在 Effect 内部使用 connection.connect(); return () => connection.disconnect(); - }, [options]); // 🚩 As a result, these dependencies are always different on a re-render + }, [options]); // 🚩 因此,这些依赖在重新渲染时总是不同的 // ... ``` -Avoid using an object created during rendering as a dependency. Instead, create the object inside the Effect: +避免使用渲染期间创建的对象作为依赖项。相反,在 Effect 内部创建对象: @@ -1553,7 +1553,7 @@ export default function App() { ```js chat.js export function createConnection({ serverUrl, roomId }) { - // A real implementation would actually connect to the server + // 真正的实现将实际连接到服务器 return { connect() { console.log('✅ Connecting to "' + roomId + '" room at ' + serverUrl + '...'); @@ -1572,21 +1572,21 @@ button { margin-left: 10px; } -Now that you create the `options` object inside the Effect, the Effect itself only depends on the `roomId` string. +现在你已经在 Effect 内部创建了 `options` 对象,因此 Effect 仅依赖于 `roomId` 字符串。 -With this fix, typing into the input doesn't reconnect the chat. Unlike an object which gets re-created, a string like `roomId` doesn't change unless you set it to another value. [Read more about removing dependencies.](/learn/removing-effect-dependencies) +通过此修复,在输入框中输入不会导致重新连接到聊天室。与会被重新创建的对象不同,像 `roomId` 这样的字符串只有在被设置为其它值时才会更改。[阅读有关删除依赖项的更多信息](/learn/removing-effect-dependencies)。 --- -### Removing unnecessary function dependencies {/*removing-unnecessary-function-dependencies*/} +### 删除不必要的函数依赖项 {/*removing-unnecessary-function-dependencies*/} -If your Effect depends on an object or a function created during rendering, it might run too often. For example, this Effect re-connects after every render because the `createOptions` function is [different for every render:](/learn/removing-effect-dependencies#does-some-reactive-value-change-unintentionally) +如果你的 Effect 依赖于在渲染期间创建的对象或函数,则它可能会频繁运行。例如,此 Effect 在每次渲染后重新连接,因为 `createOptions` 函数 [在每次渲染时都不同](/learn/removing-effect-dependencies#does-some-reactive-value-change-unintentionally): ```js {4-9,12,16} function ChatRoom({ roomId }) { const [message, setMessage] = useState(''); - function createOptions() { // 🚩 This function is created from scratch on every re-render + function createOptions() { // 🚩 此函数在每次重新渲染都从头开始创建 return { serverUrl: serverUrl, roomId: roomId @@ -1594,17 +1594,17 @@ function ChatRoom({ roomId }) { } useEffect(() => { - const options = createOptions(); // It's used inside the Effect + const options = createOptions(); // 它在 Effect 中被使用 const connection = createConnection(); connection.connect(); return () => connection.disconnect(); - }, [createOptions]); // 🚩 As a result, these dependencies are always different on a re-render + }, [createOptions]); // 🚩 因此,此依赖项在每次重新渲染都是不同的 // ... ``` -By itself, creating a function from scratch on every re-render is not a problem. You don't need to optimize that. However, if you use it as a dependency of your Effect, it will cause your Effect to re-run after every re-render. +就其本身而言,在每次重新渲染时从头新建一个函数不是问题。你不需要优化它。但是,如果你将其用作 Effect 的依赖项,则会导致 Effect 在每次重新渲染后重新运行。 -Avoid using a function created during rendering as a dependency. Instead, declare it inside the Effect: +避免使用在渲染期间创建的函数作为依赖项,请在 Effect 内部声明它: @@ -1663,7 +1663,7 @@ export default function App() { ```js chat.js export function createConnection({ serverUrl, roomId }) { - // A real implementation would actually connect to the server + // 真正的实现将实际连接到服务器 return { connect() { console.log('✅ Connecting to "' + roomId + '" room at ' + serverUrl + '...'); @@ -1682,32 +1682,32 @@ button { margin-left: 10px; } -Now that you define the `createOptions` function inside the Effect, the Effect itself only depends on the `roomId` string. With this fix, typing into the input doesn't reconnect the chat. Unlike a function which gets re-created, a string like `roomId` doesn't change unless you set it to another value. [Read more about removing dependencies.](/learn/removing-effect-dependencies) +现在你在 Effect 内部定义了 `createOptions` 函数,这样 Effect 本身只依赖于 `roomId` 字符串。通过此修复,输入框的输入不会重新连接聊天室。与被重新创建的函数不同,像 `roomId` 这样的字符串除非你将其设置为其它值,否则它不会改变。[了解更多有关移除依赖项的信息](/learn/removing-effect-dependencies)。 --- -### Reading the latest props and state from an Effect {/*reading-the-latest-props-and-state-from-an-effect*/} +### 从 Effect 读取最新的 props 和 state {/*reading-the-latest-props-and-state-from-an-effect*/} -This section describes an **experimental API that has not yet been released** in a stable version of React. +本节描述了一个 **实验性的 API**,它还没有在一个稳定的 React 版本中发布。 -By default, when you read a reactive value from an Effect, you have to add it as a dependency. This ensures that your Effect "reacts" to every change of that value. For most dependencies, that's the behavior you want. +默认情况下,在 Effect 中读取响应式值时,必须将其添加为依赖项。这样可以确保你的 Effect 对该值的每次更改都“作出响应”。对于大多数依赖项,这是你想要的行为。 -**However, sometimes you'll want to read the *latest* props and state from an Effect without "reacting" to them.** For example, imagine you want to log the number of the items in the shopping cart for every page visit: +**然而,有时你想要从 Effect 中获取 **最新的** props 和 state,而不“响应”它们**。例如,假设你想记录每次页面访问时购物车中的商品数量: ```js {3} function Page({ url, shoppingCart }) { useEffect(() => { logVisit(url, shoppingCart.length); - }, [url, shoppingCart]); // ✅ All dependencies declared + }, [url, shoppingCart]); // ✅ 所有声明的依赖项 // ... } ``` -**What if you want to log a new page visit after every `url` change, but *not* if only the `shoppingCart` changes?** You can't exclude `shoppingCart` from dependencies without breaking the [reactivity rules.](#specifying-reactive-dependencies) However, you can express that you *don't want* a piece of code to "react" to changes even though it is called from inside an Effect. [Declare an *Effect Event*](/learn/separating-events-from-effects#declaring-an-effect-event) with the [`useEffectEvent`](/reference/react/experimental_useEffectEvent) Hook, and move the code reading `shoppingCart` inside of it: +**如果你想在每次 `url` 更改后记录一次新的页面访问,而不是在 `shoppingCart` 更改后记录,该怎么办**?你不能在不违反 [响应规则](#specifying-reactive-dependencies) 的情况下将 `shoppingCart` 从依赖项中移除。然而,你可以表达你 **不希望** 某些代码对更改做出“响应”,即使它是在 Effect 内部调用的。使用 [`useEffectEvent`](/reference/react/experimental_useEffectEvent) Hook [声明 **Effect 事件**](/learn/separating-events-from-effects#declaring-an-effect-event),并将读取 `shoppingCart` 的代码移入其中: ```js {2-4,7,8} function Page({ url, shoppingCart }) { @@ -1717,23 +1717,23 @@ function Page({ url, shoppingCart }) { useEffect(() => { onVisit(url); - }, [url]); // ✅ All dependencies declared + }, [url]); // ✅ 所有声明的依赖项 // ... } ``` -**Effect Events are not reactive and must always be omitted from dependencies of your Effect.** This is what lets you put non-reactive code (where you can read the latest value of some props and state) inside of them. By reading `shoppingCart` inside of `onVisit`, you ensure that `shoppingCart` won't re-run your Effect. +**Effect 事件不是响应式的,必须始终省略其作为 Effect 的依赖项**。这就是让你在其中放置非响应式代码(可以在其中读取某些 props 和 state 的最新值)的原因。通过在 `onVisit` 中读取 `shoppingCart`,确保了 `shoppingCart` 不会使 Effect 重新运行。 -[Read more about how Effect Events let you separate reactive and non-reactive code.](/learn/separating-events-from-effects#reading-latest-props-and-state-with-effect-events) +[阅读更多关于 Effect Event 如何让你分离响应式和非响应式代码的内容](/learn/separating-events-from-effects#reading-latest-props-and-state-with-effect-events)。 --- -### Displaying different content on the server and the client {/*displaying-different-content-on-the-server-and-the-client*/} +### 在服务器和客户端上显示不同的内容 {/*displaying-different-content-on-the-server-and-the-client*/} -If your app uses server rendering (either [directly](/reference/react-dom/server) or via a [framework](/learn/start-a-new-react-project#production-grade-react-frameworks)), your component will render in two different environments. On the server, it will render to produce the initial HTML. On the client, React will run the rendering code again so that it can attach your event handlers to that HTML. This is why, for [hydration](/reference/react-dom/client/hydrateRoot#hydrating-server-rendered-html) to work, your initial render output must be identical on the client and the server. +如果你的应用程序使用服务端([直接](/reference/react-dom/server) 或通过 [框架](/learn/start-a-new-react-project#production-grade-react-frameworks))渲染,你的组件将会在两个不同的环境中渲染。在服务器上,它将渲染生成初始 HTML。在客户端,React 将再次运行渲染代码,以便将事件处理附加到该 HTML 上。这就是为什么要让 [hydration](/reference/react-dom/client/hydrateRoot#hydrating-server-rendered-html) 发挥作用,你的初始渲染输出必须在客户端和服务器上完全相同的原因。 -In rare cases, you might need to display different content on the client. For example, if your app reads some data from [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage), it can't possibly do that on the server. Here is how you could implement this: +在极少数情况下,你可能需要在客户端上显示不同的内容。例如,如果你的应用从 [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) 中读取某些数据,服务器上肯定不可能做到这一点。以下是这如何实现的: ```js function MyComponent() { @@ -1744,44 +1744,44 @@ function MyComponent() { }, []); if (didMount) { - // ... return client-only JSX ... + // ... 返回仅客户端的 JSX ... } else { - // ... return initial JSX ... + // ... 返回初始 JSX ... } } ``` -While the app is loading, the user will see the initial render output. Then, when it's loaded and hydrated, your Effect will run and set `didMount` to `true`, triggering a re-render. This will switch to the client-only render output. Effects don't run on the server, so this is why `didMount` was `false` during the initial server render. +当应用加载时,用户首先会看到初始渲染的输出。然后,当它加载完并进行 hydrate 时,Effect 将会运行并且将 `didMount` 设置为 `true`,从而触发重新渲染。这将切换到仅在客户端的渲染输出。Effect 不在服务器上运行,这就是为什么 `didMount` 在初始服务器渲染期间为 `false` 的原因。 -Use this pattern sparingly. Keep in mind that users with a slow connection will see the initial content for quite a bit of time--potentially, many seconds--so you don't want to make jarring changes to your component's appearance. In many cases, you can avoid the need for this by conditionally showing different things with CSS. +谨慎使用此模式。请记住,网络连接速度较慢的用户将在相当长的时间内(可能是数秒钟)看到初始内容,因此你不希望对组件的外观进行突兀的更改。在许多情况下,你可以通过使用 CSS 条件性地显示不同的内容来避免这种需要。 --- -## Troubleshooting {/*troubleshooting*/} +## 疑难解答 {/*troubleshooting*/} -### My Effect runs twice when the component mounts {/*my-effect-runs-twice-when-the-component-mounts*/} +### Effect 在组件挂载时运行了两次 {/*my-effect-runs-twice-when-the-component-mounts*/} -When Strict Mode is on, in development, React runs setup and cleanup one extra time before the actual setup. +在开发环境下,如果开启严格模式,React 会在实际运行 setup 之前额外运行一次 setup 和 cleanup。 -This is a stress-test that verifies your Effect’s logic is implemented correctly. If this causes visible issues, your cleanup function is missing some logic. The cleanup function should stop or undo whatever the setup function was doing. The rule of thumb is that the user shouldn’t be able to distinguish between the setup being called once (as in production) and a setup → cleanup → setup sequence (as in development). +这是一个压力测试,用于验证 Effect 的逻辑是否正确实现。如果出现可见问题,则 cleanup 函数缺少某些逻辑。cleanup 函数应该停止或撤消 setup 函数所做的任何操作。一般来说,用户不应该能够区分 setup 被调用一次(如在生产环境中)和调用 setup → cleanup → setup 序列(如在开发环境中)。 -Read more about [how this helps find bugs](/learn/synchronizing-with-effects#step-3-add-cleanup-if-needed) and [how to fix your logic.](/learn/synchronizing-with-effects#how-to-handle-the-effect-firing-twice-in-development) +阅读更多关于 [这如何帮助找到 bug](/learn/synchronizing-with-effects#step-3-add-cleanup-if-needed) 和 [如何修复你的逻辑](/learn/synchronizing-with-effects#how-to-handle-the-effect-firing-twice-in-development)。 --- -### My Effect runs after every re-render {/*my-effect-runs-after-every-re-render*/} +### Effect 在每次重新渲染后都运行 {/*my-effect-runs-after-every-re-render*/} -First, check that you haven't forgotten to specify the dependency array: +首先,请检查是否忘记指定依赖项数组: ```js {3} useEffect(() => { // ... -}); // 🚩 No dependency array: re-runs after every render! +}); // 🚩 没有依赖项数组:每次重新渲染后重新运行! ``` -If you've specified the dependency array but your Effect still re-runs in a loop, it's because one of your dependencies is different on every re-render. +如果你已经指定了依赖项数组,你的 Effect 仍循环地重新运行,那是因为你的某个依赖项在每次重新渲染时都是不同的。 -You can debug this problem by manually logging your dependencies to the console: +你可以通过手动打印依赖项到控制台来调试此问题: ```js {5} useEffect(() => { @@ -1791,58 +1791,58 @@ You can debug this problem by manually logging your dependencies to the console: console.log([serverUrl, roomId]); ``` -You can then right-click on the arrays from different re-renders in the console and select "Store as a global variable" for both of them. Assuming the first one got saved as `temp1` and the second one got saved as `temp2`, you can then use the browser console to check whether each dependency in both arrays is the same: +然后,你可以右键单击控制台中来自不同重新渲染的数组,并都选择“存储为全局变量”。假设第一个被保存为 `temp1`,第二个被保存为 `temp2`,然后你可以使用浏览器控制台来检查两个数组中的每个依赖项是否相同: ```js -Object.is(temp1[0], temp2[0]); // Is the first dependency the same between the arrays? -Object.is(temp1[1], temp2[1]); // Is the second dependency the same between the arrays? -Object.is(temp1[2], temp2[2]); // ... and so on for every dependency ... +Object.is(temp1[0], temp2[0]); // 第一个依赖项在数组之间是否相同? +Object.is(temp1[1], temp2[1]); // 第二个依赖项在数组之间是否相同? +Object.is(temp1[2], temp2[2]); // ... 以此类推检查每个依赖项 ... ``` -When you find the dependency that is different on every re-render, you can usually fix it in one of these ways: +当你发现某个依赖项在每次重新渲染都不同时,通常可以通过以下方式之一来解决: -- [Updating state based on previous state from an Effect](#updating-state-based-on-previous-state-from-an-effect) -- [Removing unnecessary object dependencies](#removing-unnecessary-object-dependencies) -- [Removing unnecessary function dependencies](#removing-unnecessary-function-dependencies) -- [Reading the latest props and state from an Effect](#reading-the-latest-props-and-state-from-an-effect) +- [在 Effect 中根据先前 state 更新 state](#updating-state-based-on-previous-state-from-an-effect) +- [删除不必要的对象依赖项](#removing-unnecessary-object-dependencies) +- [删除不必要的函数依赖项](#removing-unnecessary-function-dependencies) +- [从 Effect 读取最新的 props 和 state](#reading-the-latest-props-and-state-from-an-effect) -As a last resort (if these methods didn't help), wrap its creation with [`useMemo`](/reference/react/useMemo#memoizing-a-dependency-of-another-hook) or [`useCallback`](/reference/react/useCallback#preventing-an-effect-from-firing-too-often) (for functions). +作为最后的手段(如果这些方法没有帮助),使用 [`useMemo`](/reference/react/useMemo#memoizing-a-dependency-of-another-hook) 或 [`useCallback`](/reference/react/useCallback#preventing-an-effect-from-firing-too-often)(用于函数)包装其创建。 --- -### My Effect keeps re-running in an infinite cycle {/*my-effect-keeps-re-running-in-an-infinite-cycle*/} +### Effect 函数一直在无限循环中运行 {/*my-effect-keeps-re-running-in-an-infinite-cycle*/} -If your Effect runs in an infinite cycle, these two things must be true: +如果你的 Effect 函数一直在无限循环中运行,那么必须满足以下两个条件: -- Your Effect is updating some state. -- That state leads to a re-render, which causes the Effect's dependencies to change. +- 你的 Effect 函数更新了一些状态。 +- 这些状态的改变导致了重新渲染,从而导致 Effect 函数依赖的状态发生改变。 -Before you start fixing the problem, ask yourself whether your Effect is connecting to some external system (like DOM, network, a third-party widget, and so on). Why does your Effect need to set state? Does it synchronize with that external system? Or are you trying to manage your application's data flow with it? +在开始修复问题之前,问问自己,你的 Effect 是否连接到了某个外部系统(如 DOM、网络、第三方小部件等)。为什么你的 Effect 函数需要设置状态?它是否与外部系统同步?或者你正在试图用它来管理应用程序的数据流? -If there is no external system, consider whether [removing the Effect altogether](/learn/you-might-not-need-an-effect) would simplify your logic. +如果没有外部系统,请考虑 [完全删除 Effect 函数](/learn/you-might-not-need-an-effect) 是否可以简化你的逻辑。 -If you're genuinely synchronizing with some external system, think about why and under what conditions your Effect should update the state. Has something changed that affects your component's visual output? If you need to keep track of some data that isn't used by rendering, a [ref](/reference/react/useRef#referencing-a-value-with-a-ref) (which doesn't trigger re-renders) might be more appropriate. Verify your Effect doesn't update the state (and trigger re-renders) more than needed. +如果你真的正在与某个外部系统同步,请考虑为什么以及在何种条件下你的 Effect 函数应该更新状态。是否有任何变化会影响组件的可视输出?如果你需要跟踪一些不用于渲染的数据,使用一个 [ref](/reference/react/useRef#referencing-a-value-with-a-ref)(它不会触发重新渲染)可能更合适。验证你的 Effect 函数不会超过需要地更新状态(并触发重新渲染)。 -Finally, if your Effect is updating the state at the right time, but there is still a loop, it's because that state update leads to one of the Effect's dependencies changing. [Read how to debug dependency changes.](/reference/react/useEffect#my-effect-runs-after-every-re-render) +最后,如果你的 Effect 函数在正确的时机更新了状态,但仍然存在一个循环,那是因为该状态更新导致 Effect 的一个依赖项发生了更改。[阅读如何调试依赖项变更](/reference/react/useEffect#my-effect-runs-after-every-re-render)。 --- -### My cleanup logic runs even though my component didn't unmount {/*my-cleanup-logic-runs-even-though-my-component-didnt-unmount*/} +### 即使组件没有卸载,cleanup 逻辑也会运行 {/*my-cleanup-logic-runs-even-though-my-component-didnt-unmount*/} -The cleanup function runs not only during unmount, but before every re-render with changed dependencies. Additionally, in development, React [runs setup+cleanup one extra time immediately after component mounts.](#my-effect-runs-twice-when-the-component-mounts) +cleanup 函数不仅在卸载期间运行,也在每个依赖项变更的重新渲染前运行。此外,在开发环境中,React [在组件挂载后会立即额外运行一次 setup + cleanup](#my-effect-runs-twice-when-the-component-mounts)。 -If you have cleanup code without corresponding setup code, it's usually a code smell: +如果你的 cleanup 代码没有相应的 setup 代码,这通常是一种代码异味(code smell): ```js {2-5} useEffect(() => { - // 🔴 Avoid: Cleanup logic without corresponding setup logic + // 🔴 避免:cleanup 逻辑没有相应的 setup 逻辑 return () => { doSomething(); }; }, []); ``` -Your cleanup logic should be "symmetrical" to the setup logic, and should stop or undo whatever setup did: +你的 cleanup 逻辑应该与 setup 逻辑“对称”,并且应该停止或撤销任何 setup 做的事情: ```js {2-3,5} useEffect(() => { @@ -1854,10 +1854,10 @@ Your cleanup logic should be "symmetrical" to the setup logic, and should stop o }, [serverUrl, roomId]); ``` -[Learn how the Effect lifecycle is different from the component's lifecycle.](/learn/lifecycle-of-reactive-effects#the-lifecycle-of-an-effect) +[了解 Effect 生命周期与组件的生命周期有何不同](/learn/lifecycle-of-reactive-effects#the-lifecycle-of-an-effect)。 --- -### My Effect does something visual, and I see a flicker before it runs {/*my-effect-does-something-visual-and-i-see-a-flicker-before-it-runs*/} +### 我的 Effect 做了一些视觉相关的事情,在它运行之前我看到了一个闪烁 {/*my-effect-does-something-visual-and-i-see-a-flicker-before-it-runs*/} -If your Effect must block the browser from [painting the screen,](/learn/render-and-commit#epilogue-browser-paint) replace `useEffect` with [`useLayoutEffect`](/reference/react/useLayoutEffect). Note that **this shouldn't be needed for the vast majority of Effects.** You'll only need this if it's crucial to run your Effect before the browser paint: for example, to measure and position a tooltip before the user sees it. +如果 Effect 一定要阻止浏览器绘制屏幕,使用 [`useLayoutEffect`](/reference/react/useLayoutEffect) 替换 `useEffect`。请注意,**绝大多数的 Effect 都不需要这样**。只有当在浏览器绘制之前运行 Effect 非常重要的时候才需要如此:例如,在用户看到 tooltip 之前测量并定位它。