useEffect 是一个 React 钩子,可以让你 将组件与外部系统同步。

¥useEffect is a React Hook that lets you synchronize a component with an external system.

useEffect(setup, dependencies?)

参考

¥Reference

useEffect(setup, dependencies?)

在组件的顶层调用 useEffect 以声明一个副作用:

¥Call useEffect at the top level of your component to declare an Effect:

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

function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');

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

请参阅下面的更多示例。

¥See more examples below.

参数

¥Parameters

  • setup:具有副作用逻辑的函数。你的设置函数也可以选择返回一个清理函数。当你的组件被添加到 DOM 时,React 将运行你的设置函数。在每次使用更改的依赖重新渲染后,React 将首先使用旧值运行清理函数(如果你提供了它),然后使用新值运行你的设置函数。在你的组件从 DOM 中移除后,React 将运行你的清理函数。

    ¥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.

  • 可选 dependenciessetup 代码中引用的所有反应值的列表。反应值包括属性、状态以及直接在组件主体内声明的所有变量和函数。如果你的 linter 是 为 React 配置,它将验证每个反应值是否正确指定为依赖。依赖列表必须具有恒定数量的条目,并且像 [dep1, dep2, dep3] 一样写成内联。React 将使用 Object.is 比较将每个依赖与其先前的值进行比较。如果省略此参数,你的副作用将在每次重新渲染组件后重新运行。查看传递依赖数组、空数组和完全不依赖之间的区别。

    ¥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, 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 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.

返回

¥Returns

useEffect 返回 undefined

¥useEffect returns undefined.

注意事项

¥Caveats

  • useEffect 是一个 Hook,所以你只能在你的组件的顶层或者你自己的钩子中调用它。你不能在循环或条件内调用它。如果需要,提取一个新组件并将状态移入其中。

    ¥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.

  • 如果你不尝试与某些外部系统同步,你可能不需要副作用

    ¥If you’re not trying to synchronize with some external system, you probably don’t need an Effect.

  • 当严格模式打开时,React 将在第一次真正设置之前运行一个额外的仅开发设置+清理周期。这是一个压力测试,可确保你的清理逻辑 “mirrors” 你的设置逻辑,并确保它停止或撤消设置正在执行的任何操作。如果这导致问题,实现清理函数。

    ¥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.

  • 如果你的某些依赖是在组件内部定义的对象或函数,则存在它们会导致副作用重新运行频率超过所需频率的风险。要解决此问题,请删除不必要的 object函数 依赖。你也可以在副作用器之外进行 提取状态更新非 React 性逻辑

    ¥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 and function dependencies. You can also extract state updates and non-reactive logic outside of your Effect.

  • 如果你的副作用不是由交互(如点击)引起的,React 通常会让浏览器在运行你的副作用之前先绘制更新的屏幕。如果你的效果正在执行一些视觉操作(例如,定位工具提示),并且延迟很明显(例如,它闪烁),请将 useEffect 替换为 useLayoutEffect

    ¥If your Effect wasn’t caused by an interaction (like a click), React will generally 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.

  • 即使你的效果是由交互(例如单击)引起的,浏览器也可能会在处理效果内的状态更新之前重新绘制屏幕。通常,这就是你想要的。但是,如果必须阻止浏览器重新绘制屏幕,则需要将 useEffect 替换为 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.

  • 副作用仅在客户端上运行。它们不会在服务器渲染期间运行。

    ¥Effects only run on the client. They don’t run during server rendering.


用法

¥Usage

连接到外部系统

¥Connecting to an external system

有些组件在页面上显示时,需要与网络、某些浏览器 API 或第三方库保持连接。这些系统不受 React 控制,因此它们被称为外部系统。

¥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.

要在组件的顶层调用 将你的组件连接到某个外部系统,,请调用 useEffect

¥To connect your component to some external system, call useEffect at the top level of your component:

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

function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');

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

你需要将两个参数传递给 useEffect

¥You need to pass two arguments to useEffect:

  1. 具有连接到该系统的 setup code 的设置函数。

    ¥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.

React 会在必要时调用你的设置和清理函数,这可能会发生多次:

¥React calls your setup and cleanup functions whenever it’s necessary, which may happen multiple times:

  1. 当你的组件添加到页面(挂载)时,你的设置代码 运行。

    ¥Your setup code runs when your component is added to the page (mounts).

  2. dependencies 更改的组件的每次重新渲染之后:

    ¥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).

让我们为上面的例子说明这个顺序。

¥Let’s illustrate this sequence for the example above.

当上面的 ChatRoom 组件添加到页面时,它将连接到初始 serverUrlroomId 的聊天室。如果 serverUrlroomId 由于重新渲染而发生变化(例如,如果用户在下拉列表中选择不同的聊天室),你的副作用将与前一个房间断开连接,并连接到下一个房间。当 ChatRoom 组件从页面中删除时,你的效果将最后一次断开连接。

¥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.

对于开发中的 帮你找 bug,,React 在 setup 之前额外运行一次 setupcleanup。这是一个压力测试,用于验证你的副作用的逻辑是否正确实现。如果这导致可见问题,则说明你的清理函数缺少某些逻辑。清理函数应该停止或撤消设置函数正在做的任何事情。经验法则是用户不应该能够区分调用一次的设置(如在生产中)和设置→清理→设置序列(如在开发中)。查看常见的解决方案。

¥To help you find bugs, 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 setupcleanupsetup sequence (as in development). See common solutions.

尝试 将每个副作用写成一个独立的进程一次考虑一个设置/清理周期。 无论你的组件是安装、更新还是卸载,都无关紧要。当你的清理逻辑正确地 “mirrors” 设置逻辑时,你的副作用可以根据需要灵活地运行设置和清理。

¥Try to write every Effect as an independent process and think about a single setup/cleanup cycle at a time. 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.

注意

副作用可以让你 保持你的组件同步 与一些外部系统(比如聊天服务)。在这里,外部系统是指任何不受 React 控制的代码,例如:

¥An Effect lets you keep your component synchronized with some external system (like a chat service). Here, external system means any piece of code that’s not controlled by React, such as:

如果你没有连接到任何外部系统,你可能不需要副作用

¥If you’re not connecting to any external system, you probably don’t need an Effect.

Examples of connecting to an external system

例子 1 / 5:
连接到聊天服务器

¥Connecting to a chat server

在此示例中,ChatRoom 组件使用副作用来保持与 chat.js 中定义的外部系统的连接。按 “打开聊天” 使 ChatRoom 组件出现。此沙盒在开发模式下运行,因此有一个额外的连接和断开循环,因为 在这里解释。 尝试使用下拉菜单和输入更改 roomIdserverUrl,然后查看副作用如何重新连接到聊天。最后一次按 “关闭聊天” 查看副作用器断开连接。

¥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. 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.

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

function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://localhost:1234');

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

  return (
    <>
      <label>
        Server URL:{' '}
        <input
          value={serverUrl}
          onChange={e => setServerUrl(e.target.value)}
        />
      </label>
      <h1>Welcome to the {roomId} room!</h1>
    </>
  );
}

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


自定义钩子中的封装副作用

¥Wrapping Effects in custom Hooks

副作用是一个 “应急方案”:,当你需要 “走出 React” 并且没有更好的内置解决方案适合你的用例时,你可以使用它们。如果你发现自己经常需要手动编写副作用,这通常表明你需要为组件所依赖的常见行为提取一些 自定义钩子

¥Effects are an “escape hatch”: 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 for common behaviors your components rely on.

例如,这个 useChatRoom 自定义钩子 “hides” 你的副作用的逻辑背后是一个更具声明性的 API:

¥For example, this useChatRoom custom Hook “hides” the logic of your Effect behind a more declarative API:

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

然后你可以像这样从任何组件使用它:

¥Then you can use it from any component like this:

function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');

useChatRoom({
roomId: roomId,
serverUrl: serverUrl
});
// ...

React 生态系统中还有许多优秀的自定义钩子可用于各种用途。

¥There are also many excellent custom Hooks for every purpose available in the React ecosystem.

了解有关在自定义钩子中封装副作用的更多信息。

¥Learn more about wrapping Effects in custom Hooks.

Examples of wrapping Effects in custom Hooks

例子 1 / 3:
定制 useChatRoom 钩子

¥Custom useChatRoom Hook

此示例与 前面的例子, 中的一个相同,但逻辑被提取到自定义钩子。

¥This example is identical to one of the earlier examples, but the logic is extracted to a custom Hook.

import { useState } from 'react';
import { useChatRoom } from './useChatRoom.js';

function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://localhost:1234');

  useChatRoom({
    roomId: roomId,
    serverUrl: serverUrl
  });

  return (
    <>
      <label>
        Server URL:{' '}
        <input
          value={serverUrl}
          onChange={e => setServerUrl(e.target.value)}
        />
      </label>
      <h1>Welcome to the {roomId} room!</h1>
    </>
  );
}

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


控制非 React 小部件

¥Controlling a non-React widget

有时,你希望使外部系统与组件的某些属性或状态保持同步。

¥Sometimes, you want to keep an external system synchronized to some prop or state of your component.

例如,如果你有一个第三方地图小部件或一个没有使用 React 编写的视频播放器组件,你可以使用副作用来调用它的方法,使其状态与你的 React 组件的当前状态相匹配。这个副作用创建了一个在 map-widget.js 中定义的 MapWidget 类的实例。当你更改 Map 组件的 zoomLevel 属性时,副作用会调用类实例上的 setZoom() 以保持同步:

¥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:

import { useRef, useEffect } from 'react';
import { MapWidget } from './map-widget.js';

export default function Map({ zoomLevel }) {
  const containerRef = useRef(null);
  const mapRef = useRef(null);

  useEffect(() => {
    if (mapRef.current === null) {
      mapRef.current = new MapWidget(containerRef.current);
    }

    const map = mapRef.current;
    map.setZoom(zoomLevel);
  }, [zoomLevel]);

  return (
    <div
      style={{ width: 200, height: 200 }}
      ref={containerRef}
    />
  );
}

在此示例中,不需要清理函数,因为 MapWidget 类仅管理传递给它的 DOM 节点。从树中移除 Map React 组件后,DOM 节点和 MapWidget 类实例都会被浏览器 JavaScript 引擎自动垃圾回收。

¥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.


使用副作用请求数据

¥Fetching data with Effects

你可以使用副作用为你的组件获取数据。请注意,使用框架的数据请求机制的 如果你使用框架, 将比手动编写副作用更有效。

¥You can use an Effect to fetch data for your component. Note that if you use a framework, using your framework’s data fetching mechanism will be a lot more efficient than writing Effects manually.

如果你想手动从副作用中获取数据,你的代码可能如下所示:

¥If you want to fetch data from an Effect manually, your code might look like this:

import { useState, useEffect } from 'react';
import { fetchBio } from './api.js';

export default function Page() {
const [person, setPerson] = useState('Alice');
const [bio, setBio] = useState(null);

useEffect(() => {
let ignore = false;
setBio(null);
fetchBio(person).then(result => {
if (!ignore) {
setBio(result);
}
});
return () => {
ignore = true;
};
}, [person]);

// ...

请注意初始化为 falseignore 变量,并在清理期间设置为 true。这确保 你的代码不会受到 “竞态条件” 的影响: 网络响应可能以与你发送它们不同的顺序到达。

¥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”: network responses may arrive in a different order than you sent them.

import { useState, useEffect } from 'react';
import { fetchBio } from './api.js';

export default function Page() {
  const [person, setPerson] = useState('Alice');
  const [bio, setBio] = useState(null);
  useEffect(() => {
    let ignore = false;
    setBio(null);
    fetchBio(person).then(result => {
      if (!ignore) {
        setBio(result);
      }
    });
    return () => {
      ignore = true;
    }
  }, [person]);

  return (
    <>
      <select value={person} onChange={e => {
        setPerson(e.target.value);
      }}>
        <option value="Alice">Alice</option>
        <option value="Bob">Bob</option>
        <option value="Taylor">Taylor</option>
      </select>
      <hr />
      <p><i>{bio ?? 'Loading...'}</i></p>
    </>
  );
}

你也可以使用 async / await 语法重写,但你仍然需要提供清理函数:

¥You can also rewrite using the async / await syntax, but you still need to provide a cleanup function:

import { useState, useEffect } from 'react';
import { fetchBio } from './api.js';

export default function Page() {
  const [person, setPerson] = useState('Alice');
  const [bio, setBio] = useState(null);
  useEffect(() => {
    async function startFetching() {
      setBio(null);
      const result = await fetchBio(person);
      if (!ignore) {
        setBio(result);
      }
    }

    let ignore = false;
    startFetching();
    return () => {
      ignore = true;
    }
  }, [person]);

  return (
    <>
      <select value={person} onChange={e => {
        setPerson(e.target.value);
      }}>
        <option value="Alice">Alice</option>
        <option value="Bob">Bob</option>
        <option value="Taylor">Taylor</option>
      </select>
      <hr />
      <p><i>{bio ?? 'Loading...'}</i></p>
    </>
  );
}

直接在副作用中编写数据获取会变得重复,并且很难在以后添加缓存和服务器渲染等优化。使用自定义钩子更容易 - 你自己的或由社区维护的。

¥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.

深入研究

在副作用中请求数据的好的替代方法是什么?

¥What are good alternatives to data fetching in Effects?

在副作用中编写 fetch 调用是一个 请求数据的流行方式,尤其是在完全客户端的应用中。然而,这是一种非常手动的方法,它有很大的缺点:

¥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:

  • 副作用不在服务器上运行。这意味着初始服务器渲染的 HTML 将仅包含没有数据的加载状态。客户端计算机必须下载所有 JavaScript 并渲染你的应用,然后才发现它现在需要加载数据。这不是很有效。

    ¥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.

  • 这不是很符合人机工程学。在以一种不会出现像 竞态条件 这样的错误的方式编写 fetch 调用时,涉及到相当多的样板代码。

    ¥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.

这个缺点列表并不是 React 特有的。它适用于使用任何库在挂载上获取数据。与路由一样,要做好数据获取并非易事,因此我们推荐以下方法:

¥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 查询使用驻波比React 路由 6.4+。 你也可以构建自己的解决方案,在这种情况下,你可以在后台使用副作用,但也可以添加逻辑来删除重复请求、缓存响应和避免网络瀑布(通过预加载数据或提升 路由的数据要求)。

    ¥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 also add logic for deduplicating requests, caching responses, and avoiding network waterfalls (by preloading data or hoisting data requirements to routes).

如果这些方法都不适合你,你可以继续直接在副作用中获取数据。

¥You can continue fetching data directly in Effects if neither of these approaches suit you.


指定反应依赖

¥Specifying reactive dependencies

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

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

function ChatRoom({ roomId }) { // This is a reactive value
const [serverUrl, setServerUrl] = useState('https://localhost:1234'); // This is a reactive value too

useEffect(() => {
const connection = createConnection(serverUrl, roomId); // This Effect reads these reactive values
connection.connect();
return () => connection.disconnect();
}, [serverUrl, roomId]); // ✅ So you must specify them as dependencies of your Effect
// ...
}

如果 serverUrlroomId 发生变化,你的副作用将使用新值重新连接到聊天。

¥If either serverUrl or roomId change, your Effect will reconnect to the chat using the new values.

反应的值 包括属性以及直接在组件内部声明的所有变量和函数。由于 roomIdserverUrl 是反应值,你不能将它们从依赖中删除。如果你尝试省略它们和 你的 linter 已针对 React 正确配置,,linter 会将此标记为你需要修复的错误:

¥**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, the linter will flag this as a mistake you need to fix:

function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');

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

要删除依赖,你需要 “prove” 告诉 linter 它不需要成为依赖。 例如,你可以将 serverUrl 从组件中移出,以证明它不是反应性的并且不会在重新渲染时更改:

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

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

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

现在 serverUrl 不是一个 React 值(并且不能在重新渲染时改变),它不需要是一个依赖。如果你的副作用代码不使用任何响应值,则其依赖列表应为空 ([]):

¥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 ([]):

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

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

当你的组件的任何属性或状态发生变化时,具有空依赖的副作用 不会重新运行。

¥An Effect with empty dependencies doesn’t re-run when any of your component’s props or state change.

易犯错误

如果你有一个现有的代码库,你可能有一些抑制 linter 的副作用,如下所示:

¥If you have an existing codebase, you might have some Effects that suppress the linter like this:

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

当依赖与代码不匹配时,引入错误的风险很高。通过抑制 linter,你可以对副作用所依赖的值做出反应。而是,证明它们是不必要的。

¥When dependencies don’t match the code, there is a 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.

Examples of passing reactive dependencies

例子 1 / 3:
传递依赖数组

¥Passing a dependency array

如果你指定依赖,你的副作用将在初始渲染后以及使用更改的依赖重新渲染后运行。

¥If you specify the dependencies, your Effect runs after the initial render and after re-renders with changed dependencies.

useEffect(() => {
// ...
}, [a, b]); // Runs again if a or b are different

在下面的示例中,serverUrlroomIdReact 值,,因此它们都必须指定为依赖。因此,在下拉列表中选择不同的房间或编辑服务器 URL 输入会导致聊天重新连接。但是,由于 message 未在副作用中使用(因此它不是依赖),编辑消息不会重新连接到聊天。

¥In the below example, serverUrl and roomId are 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.

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

function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://localhost:1234');
  const [message, setMessage] = useState('');

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

  return (
    <>
      <label>
        Server URL:{' '}
        <input
          value={serverUrl}
          onChange={e => setServerUrl(e.target.value)}
        />
      </label>
      <h1>Welcome to the {roomId} room!</h1>
      <label>
        Your message:{' '}
        <input value={message} onChange={e => setMessage(e.target.value)} />
      </label>
    </>
  );
}

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


根据副作用的先前状态更新状态

¥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:

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

useEffect(() => {
const intervalId = setInterval(() => {
setCount(count + 1); // You want to increment the counter every second...
}, 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.

要解决此问题,通过 c => c + 1 状态更新器setCount

¥To fix this, pass the c => c + 1 state updater to setCount:

import { useState, useEffect } from 'react';

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

  useEffect(() => {
    const intervalId = setInterval(() => {
      setCount(c => c + 1); // ✅ Pass a state updater
    }, 1000);
    return () => clearInterval(intervalId);
  }, []); // ✅ Now count is not a dependency

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

现在你传递的是 c => c + 1 而不是 count + 1你的副作用不再需要依赖于 count 作为此修复的结果,每次 count 更改时都不需要再次清理和设置间隔。

¥Now that you’re passing c => c + 1 instead of count + 1, your Effect no longer needs to depend on count. As a result of this fix, it won’t need to cleanup and setup the interval again every time the count changes.


移除不必要的对象依赖

¥Removing unnecessary object dependencies

如果你的副作用依赖于渲染期间创建的对象或函数,则它可能会运行得太频繁。例如,此副作用在每次渲染后重新连接,因为 options 对象是 每个渲染都不同:

¥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:

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

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

const options = { // 🚩 This object is created from scratch on every re-render
serverUrl: serverUrl,
roomId: roomId
};

useEffect(() => {
const connection = createConnection(options); // It's used inside the Effect
connection.connect();
return () => connection.disconnect();
}, [options]); // 🚩 As a result, these dependencies are always different on a re-render
// ...

避免使用在渲染期间创建的对象作为依赖。而是,在副作用中创建对象:

¥Avoid using an object created during rendering as a dependency. Instead, create the object inside the Effect:

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

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

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

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

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

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

现在你在副作用中创建了 options 对象,副作用本身只依赖于 roomId 字符串。

¥Now that you create the options object inside the Effect, the Effect itself only depends on the roomId string.

通过此修复,在输入中键入内容不会重新连接聊天。与重新创建的对象不同,像 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.


删除不必要的函数依赖

¥Removing unnecessary function dependencies

如果你的副作用依赖于渲染期间创建的对象或函数,则它可能会运行得太频繁。例如,此副作用在每次渲染后重新连接,因为 createOptions 函数是 每个渲染都不同:

¥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:

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

function createOptions() { // 🚩 This function is created from scratch on every re-render
return {
serverUrl: serverUrl,
roomId: roomId
};
}

useEffect(() => {
const options = createOptions(); // It's used inside the Effect
const connection = createConnection();
connection.connect();
return () => connection.disconnect();
}, [createOptions]); // 🚩 As a result, these dependencies are always different on a re-render
// ...

就其本身而言,在每次重新渲染时从头开始创建一个函数并不是问题。你不需要优化它。但是,如果你将它用作副作用的依赖,它将导致你的副作用在每次重新渲染后重新运行。

¥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.

避免使用在渲染期间创建的函数作为依赖。而是,在副作用中声明它:

¥Avoid using a function created during rendering as a dependency. Instead, declare it inside the Effect:

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

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

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

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

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

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

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

现在你在副作用中定义了 createOptions 函数,副作用本身只依赖于 roomId 字符串。通过此修复,在输入中键入内容不会重新连接聊天。与重新创建的函数不同,像 roomId 这样的字符串不会更改,除非你将它设置为另一个值。阅读有关删除依赖的更多信息。

¥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.


从副作用中读取最新的属性和状态

¥Reading the latest props and state from an Effect

开发中

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

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

默认情况下,当你从副作用读取 React 值时,你必须将其添加为依赖。这可确保你的副作用 “reacts” 对那个值的每一次变化。对于大多数依赖,这就是你想要的行为。

¥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.

但是,有时你会想从没有 “reacting” 的副作用中读取最新的属性和状态。例如,假设你想为每次页面访问记录购物车中的商品数量:

¥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:

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

如果你想在每次 url 更改后记录一个新的页面访问,但如果只有 shoppingCart 更改,该怎么办?你不能在不破坏 React 性规则。 的情况下从依赖中排除 shoppingCart 但是,你可以表示你不希望 “react” 的一段代码发生更改,即使它是从副作用内部调用的。声明一个副作用事件useEffectEvent Hook,并在其中移动读取 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. 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 with the useEffectEvent Hook, and move the code reading shoppingCart inside of it:

function Page({ url, shoppingCart }) {
const onVisit = useEffectEvent(visitedUrl => {
logVisit(visitedUrl, shoppingCart.length)
});

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

副作用事件不是反应性的,必须始终从副作用的依赖中省略。这就是让你将非反应性代码(你可以在其中读取某些属性和状态的最新值)放入其中的原因。通过读取 onVisit 内部的 shoppingCart,你可以确保 shoppingCart 不会重新运行你的副作用。

¥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.

详细了解副作用事件如何让你分离 React 性和非 React 性代码。

¥Read more about how Effect Events let you separate reactive and non-reactive code.


在服务器和客户端显示不同的内容

¥Displaying different content on the server and the client

如果你的应用使用服务器渲染(directly 或通过 框架),你的组件将在两种不同的环境中渲染。在服务器上,它将渲染以生成初始 HTML。在客户端,React 将再次运行渲染代码,以便它可以将你的事件处理程序附加到该 HTML。这就是为什么要使 hydration 正常工作,你的初始渲染输出在客户端和服务器上必须相同。

¥If your app uses server rendering (either directly or via a framework), 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 to work, your initial render output must be identical on the client and the server.

在极少数情况下,你可能需要在客户端上显示不同的内容。例如,如果你的应用从 localStorage 读取一些数据,它不可能在服务器上执行此操作。以下是你如何实现这一点:

¥In rare cases, you might need to display different content on the client. For example, if your app reads some data from localStorage, it can’t possibly do that on the server. Here is how you could implement this:

function MyComponent() {
const [didMount, setDidMount] = useState(false);

useEffect(() => {
setDidMount(true);
}, []);

if (didMount) {
// ... return client-only JSX ...
} else {
// ... return initial JSX ...
}
}

加载应用时,用户将看到初始渲染输出。然后,当它被加载和水合时,你的副作用将运行并将 didMount 设置为 true,触发重新渲染。这将切换到仅限客户端的渲染输出。副作用不在服务器上运行,所以这就是为什么在初始服务器渲染期间 didMountfalse 的原因。

¥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.

请谨慎使用此模式。请记住,连接速度较慢的用户会在相当长的一段时间内看到初始内容 - 可能是很多秒 - 所以你不想对组件的外观进行不和谐的更改。在许多情况下,你可以通过使用 CSS 有条件地显示不同的内容来避免这种需要。

¥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.


故障排除

¥Troubleshooting

我的副作用在组件挂载时运行两次

¥My Effect runs twice when the component mounts

当严格模式打开时,在开发中,React 在实际设置之前额外运行一次设置和清理。

¥When Strict Mode is on, in development, React runs setup and cleanup one extra time before the actual 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).

阅读有关 这如何帮助发现错误如何修复你的逻辑 的更多信息。

¥Read more about how this helps find bugs and how to fix your logic.


我的副作用在每次重新渲染后运行

¥My Effect runs after every re-render

首先,检查你是否没有忘记指定依赖数组:

¥First, check that you haven’t forgotten to specify the dependency array:

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.

你可以通过手动将依赖记录到控制台来调试此问题:

¥You can debug this problem by manually logging your dependencies to the console:

useEffect(() => {
// ..
}, [serverUrl, roomId]);

console.log([serverUrl, roomId]);

然后,你可以在控制台中右键单击来自不同重新渲染的数组,并为它们选择 “存储为全局变量”。假设第一个保存为 temp1,第二个保存为 temp2,那么你可以使用浏览器控制台检查两个数组中的每个依赖是否相同:

¥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:

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 ...

当你发现每次重新渲染时都不同的依赖时,你通常可以通过以下方式之一修复它:

¥When you find the dependency that is different on every re-render, you can usually fix it in one of these ways:

作为最后的手段(如果这些方法没有帮助),用 useMemouseCallback(对于函数)封装它的创建。

¥As a last resort (if these methods didn’t help), wrap its creation with useMemo or useCallback (for functions).


我的副作用在无限循环中不断重新运行

¥My Effect keeps re-running in an infinite cycle

如果你的副作用以无限循环运行,则以下两点必须为真:

¥If your Effect runs in an infinite cycle, these two things must be true:

  • 你的副作用正在更新一些状态。

    ¥Your Effect is updating some state.

  • 该状态会导致重新渲染,从而导致副作用的依赖发生变化。

    ¥That state leads to a re-render, which causes the Effect’s dependencies to change.

在开始解决问题之前,先问问自己副作用是否连接到某个外部系统(如 DOM、网络、第三方小部件等)。为什么你的副作用需要设置状态?它是否与该外部系统同步?或者你是否正在尝试使用它来管理应用的数据流?

¥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?

如果没有外部系统,请考虑 完全删除副作用 是否会简化你的逻辑。

¥If there is no external system, consider whether removing the Effect altogether would simplify your logic.

如果你真正与某个外部系统同步,请考虑你的副作用应该更新状态的原因和条件。有什么改变影响了你的组件的视觉输出吗?如果你需要跟踪渲染未使用的某些数据,引用(不会触发重新渲染)可能更合适。验证你的副作用不会比需要更多地更新状态(并触发重新渲染)。

¥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 (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.

最后,如果你的副作用在正确的时间更新状态,但仍然存在循环,那是因为该状态更新导致副作用的依赖之一发生变化。阅读如何调试依赖更改。

¥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.


即使我的组件没有卸载,我的清理逻辑仍在运行

¥My cleanup logic runs even though my component didn’t unmount

清理函数不仅在卸载期间运行,而且在每次重新渲染更改依赖之前运行。此外,在开发中,React 在组件挂载后立即额外运行一次设置+清理。

¥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.

如果你有清理代码而没有相应的设置代码,通常是代码味道:

¥If you have cleanup code without corresponding setup code, it’s usually a code smell:

useEffect(() => {
// 🔴 Avoid: Cleanup logic without corresponding setup logic
return () => {
doSomething();
};
}, []);

你的清理逻辑应该是设置逻辑的 “symmetrical”,并且应该停止或撤消任何设置所做的事情:

¥Your cleanup logic should be “symmetrical” to the setup logic, and should stop or undo whatever setup did:

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

了解副作用生命周期与组件生命周期有何不同。

¥Learn how the Effect lifecycle is different from the component’s lifecycle.


我的副作用做了一些视觉副作用,我在它运行前看到了闪烁

¥My Effect does something visual, and I see a flicker before it runs

如果你的副作用必须阻止浏览器访问 绘画屏幕,,请将 useEffect 替换为 useLayoutEffect。请注意,绝大多数效果都不需要这样做。仅当在浏览器绘制之前运行效果至关重要时,你才需要它:例如,在用户看到之前测量和定位工具提示。

¥If your Effect must block the browser from painting the screen, replace useEffect with 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.


React 中文网 - 粤ICP备13048890号