useEffect 是一个 React Hook,它可以让你 将组件与外部系统同步。
useEffect(setup, dependencies?)参考
🌐 Reference
useEffect(setup, dependencies?)
在组件的顶层调用 useEffect 来声明一个 Effect:
🌐 Call useEffect at the top level of your component to declare an Effect:
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();
};
}, [serverUrl, roomId]);
// ...
}参数
🌐 Parameters
setup:具有你 Effect 逻辑的函数。你的 setup 函数也可以选择性地返回一个 cleanup 函数。当你的 组件提交 时,React 会运行你的 setup 函数。在每次有依赖更改的提交之后,React 会先使用旧值运行 cleanup 函数(如果你提供了它),然后使用新值运行你的 setup 函数。在你的组件从 DOM 中被移除后,React 会运行你的 cleanup 函数。- 可选
dependencies:在setup代码中引用的所有响应式值的列表。响应式值包括 props、state 以及在组件主体中直接声明的所有变量和函数。如果你的代码检查器已配置为 React,它会验证每个响应式值是否正确指定为依赖。依赖列表必须有固定数量的项,并且像[dep1, dep2, dep3]一样以内联形式书写。React 会使用Object.is比较将每个依赖与之前的值进行比较。如果省略此参数,每次组件提交后,你的 Effect 都会重新运行。请参阅传递依赖数组、空数组和完全不传依赖之间的区别。
返回
🌐 Returns
“useEffect”返回“undefined”。
注意事项
🌐 Caveats
useEffect是一个 Hook,因此你只能在你的组件的顶层或你自己的 Hook 中调用它。你不能在循环或条件语句中调用它。如果你需要那样做,可以提取一个新组件并将状态移入其中。- 如果你不打算与某个外部系统同步,你可能不需要一个效果。
- 当严格模式开启时,React 会在第一次真正的设置之前运行一次额外的仅限开发的设置+清理周期。这是一个压力测试,用于确保你的清理逻辑“镜像”你的设置逻辑,并且停止或撤销设置正在执行的操作。如果这导致问题,请实现清理函数。
- 如果你的一些依赖是组件内部定义的对象或函数,就有可能**导致 Effect 比预期更频繁地重新运行。**要解决这个问题,请移除不必要的对象和函数依赖。你也可以将状态更新和非响应式逻辑提取到 Effect 外部。
- 如果你的 Effect 不是由交互(例如点击)引起的,React 通常会让浏览器**先渲染更新后的屏幕,然后再运行你的 Effect。**如果你的 Effect 做了一些视觉上的操作(例如,定位提示框),并且延迟是明显的(例如,它闪烁),请将
useEffect替换为useLayoutEffect. - 如果你的 Effect 是由交互触发的(例如点击),React 可能会在浏览器绘制更新的屏幕之前运行你的 Effect。这确保了 Effect 的结果可以被事件系统观察到。通常,这种方式可以正常工作。然而,如果你必须将工作延迟到绘制之后,例如
alert(),你可以使用setTimeout。更多信息请参见 reactwg/react-18/128。 - 即使你的 Effect 是由交互(例如点击)引起的,React 可能会在处理 Effect 内的状态更新之前允许浏览器重绘屏幕。 通常情况下,这会按预期工作。不过,如果你必须阻止浏览器重绘屏幕,你需要将
useEffect替换为useLayoutEffect. - 效果仅在客户端运行。 它们在服务器渲染期间不会运行。
用法
🌐 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 { 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();
};
}, [serverUrl, roomId]);
// ...
}你需要向 useEffect 传递两个参数:
🌐 You need to pass two arguments to useEffect:
- 一个设置函数,包含连接到该系统的 设置代码 。
- 它应该返回一个清理函数,其中包含 清理代码 ,用以断开与该系统的连接。
- 一个依赖列表,包括在这些函数中使用的组件中的每一个值。
React 会在必要时调用你的设置和清理函数,这可能会发生多次:
- 当你的组件被添加到页面上(挂载)时,你的 设置代码 会运行。
- 在每次提交你的组件时,如果 依赖 发生了变化:
- 首先,你的 清理代码 会使用旧的 props 和 state 运行。
- 然后,你的 设置代码 会使用新的属性和状态运行。
- 当你的组件从页面上被移除(卸载)后,你的 清理代码 将最后一次运行。
让我们为上面的例子来说明这个序列。
当上面的 ChatRoom 组件被添加到页面时,它将使用初始的 serverUrl 和 roomId 连接到聊天室。如果由于提交而 serverUrl 或 roomId 发生变化(例如,如果用户在下拉菜单中选择了不同的聊天室),你的 Effect 将断开与以前聊天室的连接,并连接到下一个聊天室。 当 ChatRoom 组件从页面中移除时,你的 Effect 将最后一次断开连接。
🌐 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 commit (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.
为了帮助你发现错误, 在开发模式下,React 会在 setup之前多执行一次 setup 和 cleanup 。 这是一个压力测试,用于验证你的 Effect 逻辑是否正确实现。如果这导致可见问题,说明你的 cleanup 函数缺少一些逻辑。cleanup 函数应该停止或撤销 setup 函数所做的任何操作。经验法则是,用户不应该能够区分 setup 被调用一次(如在生产环境中)与 setup → cleanup → setup 序列(如在开发环境中)。查看常见解决方案
尝试将每个 Effect 写成独立的过程,并且一次只考虑一个设置/清理周期。无论你的组件是挂载、更新还是卸载,这都不应有影响。当你的清理逻辑正确地“镜像”设置逻辑时,你的 Effect 就能承受按需反复执行设置和清理的情况。
例子 1 of 5: 连接到聊天服务器
🌐 Connecting to a chat server
在这个示例中,ChatRoom 组件使用 Effect 来保持与 chat.js 中定义的外部系统的连接。按下“打开聊天”以显示 ChatRoom 组件。此沙盒在开发模式下运行,所以会有一个额外的连接和断开循环,如 这里所解释。尝试使用下拉菜单和输入框更改 roomId 和 serverUrl,并观察 Effect 如何重新连接到聊天。按下“关闭聊天”以查看 Effect 最后一次断开连接。
🌐 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
Effects 是一个“逃生出口”:当你需要“跳出 React”,而且没有更好的内置解决方案适用于你的使用场景时,你就会使用它们。如果你发现自己经常需要手动编写 Effects,通常这是一个信号,表明你需要为组件所依赖的常见行为提取一些自定义 Hooks。”
🌐 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 自定义 Hook 将你的 Effect 的逻辑“隐藏”在更声明性的 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.
例子 1 of 3: 自定义 useChatRoom 钩子
🌐 Custom useChatRoom Hook
这个例子与之前的一个例子完全相同,但逻辑被提取到一个自定义 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 编写的视频播放器组件,你可以使用 Effect 调用其方法,使其状态与当前 React 组件的状态保持一致。这个 Effect 会创建一个在 map-widget.js 中定义的 MapWidget 类的实例。当你更改 Map 组件的 zoomLevel 属性时,Effect 会调用类实例上的 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
你可以使用 Effect 为你的组件获取数据。请注意,如果你使用框架, 使用框架的数据获取机制会比手动编写 Effect 高效得多。
🌐 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]);
// ...注意 ignore 变量,它被初始化为 false,并在清理过程中设置为 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> </> ); }
直接在 Effects 中编写数据获取会变得重复,并且使得以后添加如缓存和服务器渲染等优化变得困难。使用自定义 Hook(无论是你自己的还是社区维护的)会更容易。
🌐 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?
在 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:
- Effect 不会在服务器上运行。 这意味着初始的服务器渲染 HTML 将只包含一个没有数据的加载状态。客户端计算机将不得不下载所有 JavaScript 并渲染你的应用,然后才会发现现在它需要加载数据。这效率不高。
- 在 Effects 中直接获取数据使创建“网络瀑布”变得容易。 你渲染父组件,它获取一些数据,渲染子组件,然后它们开始获取各自的数据。如果网络速度不快,这比并行获取所有数据要慢得多。
- 在 Effects 中直接获取通常意味着你不会预加载或缓存数据。 例如,如果组件卸载然后再次挂载,它将不得不再次获取数据。
- 这不是很符合人机工程学。 在编写
fetch调用时,需要编写相当多的样板代码,以避免像 竞态条件 这样的错误。
这个缺点列表并不特定于 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 框架已经集成了高效的数据获取机制,并且不会遇到上述问题。
- 否则,考虑使用或构建客户端缓存。 常见的开源解决方案包括 TanStack Query、useSWR 和 React Router 6.4+。你也可以自己构建解决方案,在这种情况下,你会在底层使用 Effects,但同时还需要添加用于请求去重、响应缓存以及避免网络瀑布(通过预加载数据或将数据需求提升到路由)的逻辑。
如果这些方法都不适合你,你可以继续直接在副作用中获取数据。
🌐 You can continue fetching data directly in Effects if neither of these approaches suit you.
指定反应依赖
🌐 Specifying reactive dependencies
请注意,你不能“选择”Effect的依赖。 每一个 反应值 ,都必须在Effect的代码中使用并声明为依赖。Effect的依赖列表由其周围的代码决定:
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
// ...
}如果 serverUrl 或 roomId 发生变化,你的 Effect 会使用新值重新连接到聊天。
🌐 If either serverUrl or roomId change, your Effect will reconnect to the chat using the new values.
响应式值 包括 props 以及在你的组件中直接声明的所有变量和函数。 由于 roomId 和 serverUrl 是响应式值,你不能将它们从依赖中移除。如果你尝试省略它们,并且 你的 linter 已正确为 React 配置,linter 会将此标记为你需要修复的错误:
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'
// ...
}要移除依赖,你需要向 linter “证明” 它不需要作为依赖。 例如,你可以将 serverUrl 移出组件,以证明它不是响应式的,并且在重新渲染时不会变化:
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 不是响应式值(并且在重新渲染时不会改变),它就不需要作为依赖。如果你的 Effect 代码不使用任何响应式值,其依赖列表应该为空([]):
🌐 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)在组件的任何 props 或状态改变时都不会重新运行。
例子 1 of 3: 传递依赖数组
🌐 Passing a dependency array
如果你指定了依赖,你的 Effect 会在 初始提交之后以及依赖发生变化的提交之后 运行。
🌐 If you specify the dependencies, your Effect runs after the initial commit and after commits with changed dependencies.
useEffect(() => {
// ...
}, [a, b]); // Runs again if a or b are different在下面的示例中,serverUrl 和 roomId 是响应式值, 因此它们都必须被指定为依赖。结果是在下拉菜单中选择不同的房间或编辑服务器 URL 输入会导致聊天重新连接。然而,由于 message 没有在 Effect 中使用(因此它不是依赖),编辑消息不会重新连接聊天。
🌐 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 变化时 Effect 都会清理并重新设置。这并不理想。
🌐 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,你的 Effect 不再需要依赖 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
如果你的 Effect 依赖于在渲染期间创建的对象或函数,它可能会运行得太频繁。例如,这个 Effect 会在每次提交后重新连接,因为 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 commit 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 commit
// ...避免将渲染过程中创建的对象用作依赖。相反,应在 Effect 内部创建该对象:
🌐 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} /> </> ); }
既然你在 Effect 内部创建了 options 对象,Effect 本身只依赖于 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
如果你的 Effect 依赖于在渲染期间创建的对象或函数,它可能会运行得太频繁。例如,这个 Effect 会在每次提交后重新连接,因为 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 commit 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 commit
// ...仅仅是每次重新渲染时从头创建一个函数本身并不是问题。你不需要对其进行优化。然而,如果你将其用作 Effect 的依赖,它会导致你的 Effect 在每次提交后重新运行。
🌐 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 commit.
避免将渲染期间创建的函数用作依赖。相反,应在 Effect 内部声明它:
🌐 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} /> </> ); }
既然你在 Effect 内定义了 createOptions 函数,Effect 本身只依赖于 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
默认情况下,当你从一个 Effect 中读取一个响应式值时,你必须将其添加为依赖。这确保了你的 Effect 会对该值的每一次变化做出“反应”。对于大多数依赖来说,这正是你所希望的行为。
🌐 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 中读取最新的 props 和 state,而不去“响应”它们。 例如,假设你想在每次页面访问时记录购物车中项目的数量:
function Page({ url, shoppingCart }) {
useEffect(() => {
logVisit(url, shoppingCart.length);
}, [url, shoppingCart]); // ✅ All dependencies declared
// ...
}如果你想在每次 url 变化后记录新的页面访问,但 不 在仅 shoppingCart 变化时记录,会怎样? 你不能在不破坏响应性规则的情况下排除 shoppingCart 作为依赖。然而,你可以表示你 不希望 某段代码对变化做出“响应”,即使它是在 Effect 内部调用的。使用 useEffectEvent Hook 声明一个 Effect Event,并将读取 shoppingCart 的代码移动到其中:
function Page({ url, shoppingCart }) {
const onVisit = useEffectEvent(visitedUrl => {
logVisit(visitedUrl, shoppingCart.length)
});
useEffect(() => {
onVisit(url);
}, [url]); // ✅ All dependencies declared
// ...
}Effect 事件不是响应式的,必须始终从你的 Effect 的依赖中省略。 这使你能够在其中放入非响应式代码(在这些代码中你可以读取某些 props 和 state 的最新值)。通过在 onVisit 中读取 shoppingCart,你可以确保 shoppingCart 不会重新运行你的 Effect。
了解更多关于 Effect Events 如何让你分离响应式和非响应式代码的信息。
在服务器和客户端显示不同的内容
🌐 Displaying different content on the server and the client
如果你的应用使用服务器端渲染(无论是直接还是通过框架),你的组件将会在两个不同的环境中渲染。在服务器上,它会渲染以生成初始的 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 ...
}
}在应用加载时,用户将看到初始渲染输出。然后,当它加载并完成水合时,你的 Effect 将运行并将 didMount 设置为 true,触发重新渲染。这将切换到仅客户端的渲染输出。Effect 不会在服务器上运行,所以这就是为什么在初始服务器渲染期间 didMount 是 false。
🌐 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.
这是一个压力测试,用于验证你的 Effect 逻辑是否正确实现。如果这导致可见的问题,说明你的清理函数缺少一些逻辑。清理函数应该停止或撤销设置函数正在做的任何操作。经验法则是,用户不应该能够区分设置被调用一次(如在生产环境中)和设置 → 清理 → 设置的序列(如在开发环境中)。
🌐 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 commit!如果你指定了依赖数组,但你的副作用仍然在循环中重新运行,那是因为你的一个依赖在每次重新渲染时都不同。
🌐 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:
作为最后手段(如果这些方法无效),可以用 useMemo 或 useCallback(针对函数)来封装其创建。
🌐 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:
- 你的副作用正在更新一些状态。
- 该状态会导致重新渲染,从而导致副作用的依赖发生变化。
在你开始解决问题之前,先问问自己,你的 Effect 是否正在连接某个外部系统(比如 DOM、网络、第三方小部件等)。你的 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?
如果没有外部系统,请考虑是否完全移除该效果会使你的逻辑更简单。
🌐 If there is no external system, consider whether removing the Effect altogether would simplify your logic.
如果你确实在与某个外部系统同步,请思考为什么以及在什么条件下你的 Effect 应该更新状态。是否有某些变化会影响你的组件的视觉输出?如果你需要跟踪一些不被渲染使用的数据,使用 ref(不会触发重渲染)可能更合适。请确认你的 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 (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 在正确的时间更新状态,但仍然出现循环,这是因为该状态更新导致 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.
即使我的组件没有卸载,我的清理逻辑仍在运行
🌐 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();
};
}, []);你的清理逻辑应该与设置逻辑“对称”,并且应停止或撤销设置所做的任何操作:
🌐 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]);我的副作用做了一些视觉副作用,我在它运行前看到了闪烁
🌐 My Effect does something visual, and I see a flicker before it runs
如果你的 Effect 必须阻止浏览器渲染屏幕, 请将 useEffect 替换为 useLayoutEffect。请注意,绝大多数 Effect 并不需要这样做。 你只有在需要在浏览器渲染之前运行 Effect 才需要这样做:例如,在用户看到提示之前测量和定位提示。
🌐 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.