useEffect
是一个 React Hook,允许你将组件与外部系统同步。
useEffect(setup, dependencies?)
参考
useEffect(setup, dependencies?)
在组件的顶层调用useEffect
来声明一个Effect
import { useEffect } from 'react';
import { createConnection } from './chat.js';
function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://127.0.0.1:1234');
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [serverUrl, roomId]);
// ...
}
参数
-
setup
: 包含Effect逻辑的函数。你的setup函数也可以选择返回一个cleanup函数。当你的组件添加到DOM时,React将运行你的setup函数。在每次重新渲染(依赖项发生变化)后,React将首先使用旧值运行cleanup函数(如果提供了),然后使用新值运行setup函数。在你的组件从DOM中移除后,React将运行你的cleanup函数。 -
可选
dependencies
:setup
代码中引用的所有反应式值的列表。反应式值包括 props、状态以及在组件主体中直接声明的所有变量和函数。如果您的代码检查工具已针对 React 配置,它将验证每个反应式值是否正确指定为依赖项。依赖项列表必须具有恒定的项数,并像[dep1, dep2, dep3]
一样内联编写。React 将使用Object.is
比较将每个依赖项与其之前的数值进行比较。如果您省略此参数,则您的 Effect 将在组件每次重新渲染后重新运行。查看传递依赖项数组、空数组和根本不传递依赖项之间的区别。
返回
useEffect
返回 undefined
。
注意事项
-
useEffect
是一个 Hook,因此您只能在组件的顶层或您自己的 Hook 中调用它。您不能在循环或条件内调用它。如果您需要这样做,请提取一个新组件并将状态移入其中。 -
如果您没有尝试与某些外部系统同步,您可能不需要 Effect。
-
当启用严格模式时,React 将在第一次实际设置之前运行一个额外的仅限开发的设置+清理周期。这是一个压力测试,用于确保您的清理逻辑“镜像”您的设置逻辑,并且它会停止或撤消设置正在执行的操作。如果这会导致问题,实现清理函数。
-
如果您的某些依赖项是在组件内部定义的对象或函数,则它们可能会导致 Effect 比需要的更频繁地重新运行。要解决此问题,请删除不必要的对象和函数依赖项。您还可以提取状态更新和非反应式逻辑到 Effect 之外。
-
如果您的 Effect 不是由交互(例如单击)引起的,React 通常会让浏览器先绘制更新后的屏幕,然后再运行您的 Effect。如果您的 Effect 正在执行某些视觉操作(例如,定位工具提示),并且延迟很明显(例如,它会闪烁),请将
useEffect
替换为useLayoutEffect
。 -
如果您的 Effect 是由交互(例如单击)引起的,React 可能会在浏览器绘制更新后的屏幕之前运行您的 Effect。这确保了事件系统可以观察到 Effect 的结果。通常情况下,这可以按预期工作。但是,如果您必须推迟工作到绘制之后,例如
alert()
,您可以使用setTimeout
。有关更多信息,请参见 reactwg/react-18/128。 -
即使您的 Effect 是由交互(例如单击)引起的,React 也可能会允许浏览器在处理 Effect 内部的状态更新之前重新绘制屏幕。通常情况下,这可以按预期工作。但是,如果您必须阻止浏览器重新绘制屏幕,则需要将
useEffect
替换为useLayoutEffect
。 -
Effect 仅在客户端运行。它们不会在服务器渲染期间运行。
用法
连接到外部系统
某些组件需要在页面上显示时保持与网络、某些浏览器 API 或第三方库的连接。这些系统不受 React 控制,因此称为外部系统。
要将您的组件连接到某个外部系统,请在组件的顶层调用 useEffect
import { useEffect } from 'react';
import { createConnection } from './chat.js';
function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://127.0.0.1:1234');
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [serverUrl, roomId]);
// ...
}
您需要向 useEffect
传递两个参数
- 一个带有 设置代码 的设置函数,用于连接到该系统。
- 它应该返回一个带有 清理代码 的清理函数,用于断开与该系统的连接。
- 一个包含这些函数内部使用的组件中每个值的 依赖项列表。
React 会在必要时调用您的设置和清理函数,这可能会发生多次
- 您的 设置代码 在您的组件添加到页面(挂载)时运行。
- 在每次重新渲染您的组件且依赖项发生变化之后
- 首先,您的清理代码 使用旧的 props 和状态运行。
- 然后,您的设置代码 使用新的 props 和状态运行。
- 您的清理代码 在您的组件从页面移除(卸载)后最后运行一次。
让我们为上面的示例说明此顺序。
当上面的ChatRoom
组件添加到页面时,它将使用初始的serverUrl
和roomId
连接到聊天室。如果由于重新渲染而导致serverUrl
或roomId
更改(例如,如果用户在下拉菜单中选择不同的聊天室),您的Effect将断开与先前房间的连接,并连接到下一个房间。当ChatRoom
组件从页面中移除时,您的Effect将最后一次断开连接。
为了帮助您查找错误,在开发过程中,React 会额外运行一次设置和清理。这是一个压力测试,用于验证您的 Effect 的逻辑是否正确实现。如果这会导致可见问题,则您的清理函数缺少某些逻辑。清理函数应停止或撤消设置函数正在执行的操作。经验法则是,用户不应该能够区分设置被调用一次(如在生产环境中)和设置→清理→设置序列(如在开发环境中)。查看常见解决方案。
尝试将每个 Effect 编写为独立进程并一次考虑单个设置/清理周期。无论您的组件是正在挂载、更新还是卸载,这都不重要。当您的清理逻辑正确地“镜像”设置逻辑时,您的 Effect 能够承受根据需要多次运行设置和清理。
示例 1的 5: 连接到聊天服务器
在这个例子中,ChatRoom
组件使用Effect来保持与chat.js
中定义的外部系统连接。按下“打开聊天”以显示ChatRoom
组件。此沙箱在开发模式下运行,因此存在额外的连接和断开周期,如此处所述。尝试使用下拉菜单和输入更改roomId
和serverUrl
,并查看 Effect 如何重新连接到聊天。按下“关闭聊天”以查看 Effect 最后一次断开连接。
import { useState, useEffect } from 'react'; import { createConnection } from './chat.js'; function ChatRoom({ roomId }) { const [serverUrl, setServerUrl] = useState('https://127.0.0.1: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} />} </> ); }
自定义 Hook 中的包裹效果
Effects 是一个“逃生舱”: 当你需要“跳出 React”并且没有更好的内置解决方案来应对你的用例时,你就会用到它们。如果你发现自己经常需要手动编写 Effects,这通常意味着你需要为你的组件依赖的常用行为提取一些自定义 Hook。
例如,这个useChatRoom
自定义 Hook 将 Effect 的逻辑隐藏在一个更声明式的 API 后面。
function useChatRoom({ serverUrl, roomId }) {
useEffect(() => {
const options = {
serverUrl: serverUrl,
roomId: roomId
};
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [roomId, serverUrl]);
}
然后你可以像这样从任何组件中使用它。
function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://127.0.0.1:1234');
useChatRoom({
roomId: roomId,
serverUrl: serverUrl
});
// ...
React 生态系统中还有许多针对各种用途的优秀自定义 Hook。
示例 1的 3: 自定义useChatRoom
Hook
此示例与前面的示例之一相同,但逻辑已提取到自定义 Hook 中。
import { useState } from 'react'; import { useChatRoom } from './useChatRoom.js'; function ChatRoom({ roomId }) { const [serverUrl, setServerUrl] = useState('https://127.0.0.1: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 小部件
有时,你想让外部系统与组件的某些属性或状态保持同步。
例如,如果你有一个第三方地图小部件或一个非 React 编写的视频播放器组件,你可以使用 Effect 来调用其方法,使其状态与 React 组件的当前状态匹配。此 Effect 创建一个MapWidget
类(在map-widget.js
中定义)的实例。当你更改Map
组件的zoomLevel
属性时,Effect 会调用类实例上的setZoom()
方法以保持同步。
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 引擎自动垃圾回收。
使用 Effects 获取数据
你可以使用 Effect 来获取组件的数据。如果你使用框架, 使用框架的数据获取机制将比手动编写 Effects 效率高得多。
如果你想手动从 Effect 获取数据,你的代码可能如下所示:
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
。这确保了你的代码不会出现“竞争条件”: 网络响应的到达顺序可能与你发送它们的顺序不同。
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
语法重写,但你仍然需要提供一个清理函数。
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(无论是你自己创建的还是社区维护的)更容易。
深入探讨
在 Effects 中编写 fetch
调用是一种 常用的获取数据方法,尤其是在完全客户端的应用程序中。然而,这是一种非常手动的方法,并且有很大的缺点。
- Effects 不会在服务器端运行。这意味着初始服务器端渲染的 HTML 只包含加载状态,没有数据。客户端计算机必须下载所有 JavaScript 并渲染应用程序,然后才能发现现在需要加载数据。这效率不高。
- 直接在 Effects 中获取数据很容易导致“网络瀑布”现象。你渲染父组件,它获取一些数据,渲染子组件,然后子组件开始获取它们的数据。如果网络速度不快,这比并行获取所有数据慢得多。
- 直接在 Effects 中获取数据通常意味着你不会预加载或缓存数据。例如,如果组件卸载然后再次挂载,它将不得不再次获取数据。
- 它不够方便。在编写
fetch
调用时,会涉及相当多的样板代码,而且容易出现诸如 竞争条件之类的错误。
这些缺点并不特定于 React。它适用于使用任何库在挂载时获取数据。与路由一样,数据获取并不容易做好,因此我们推荐以下方法。
- 如果你使用 框架,请使用其内置的数据获取机制。现代 React 框架集成了高效且不会出现上述缺陷的数据获取机制。
- 否则,请考虑使用或构建客户端缓存。流行的开源解决方案包括 React Query、useSWR 和 React Router 6.4+。你也可以构建自己的解决方案,在这种情况下,你将在底层使用 Effects,但也会添加逻辑来对请求进行重复数据删除、缓存响应以及避免网络瀑布(通过预加载数据或将数据需求提升到路由)。
如果这些方法都不适合你,你可以继续直接在 Effects 中获取数据。
指定反应式依赖项
注意,你无法“选择”Effect 的依赖项。Effect 代码中使用的每个 反应式值 都必须声明为依赖项。Effect 的依赖项列表由周围的代码决定。
function ChatRoom({ roomId }) { // This is a reactive value
const [serverUrl, setServerUrl] = useState('https://127.0.0.1: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 将使用新值重新连接到聊天。
反应式值 包括 props 以及在组件内部直接声明的所有变量和函数。由于 roomId
和 serverUrl
是反应式值,你不能将它们从依赖项中移除。如果你尝试省略它们,并且 你的 linter 已针对 React 正确配置,则 linter 会将此标记为你需要修复的错误。
function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://127.0.0.1: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://127.0.0.1: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 的代码不使用任何反应式值,则其依赖项列表应为空 ([]
):
const serverUrl = 'https://127.0.0.1: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
// ...
}
具有空依赖项的 Effect 不会在组件的任何 props 或状态更改时重新运行。
示例 1的 3: 传递依赖项数组
如果你指定了依赖项,你的 Effect 将在初始渲染之后以及依赖项更改后的重新渲染之后运行。
useEffect(() => {
// ...
}, [a, b]); // Runs again if a or b are different
在下面的示例中,serverUrl
和 roomId
是 反应式值,因此它们都必须指定为依赖项。结果,在下拉菜单中选择不同的房间或编辑服务器 URL 输入将导致聊天重新连接。但是,由于 message
未在 Effect 中使用(因此它不是依赖项),编辑消息不会重新连接到聊天。
import { useState, useEffect } from 'react'; import { createConnection } from './chat.js'; function ChatRoom({ roomId }) { const [serverUrl, setServerUrl] = useState('https://127.0.0.1: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}/>} </> ); }
基于 Effect 中之前的状态更新状态
当你想基于 Effect 中之前的状态更新状态时,你可能会遇到一个问题。
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
是一个响应式值,它必须在依赖项列表中指定。但是,这会导致 Effect 在每次count
变化时都进行清理和重新设置。这不是理想的。
为了解决这个问题,将c => c + 1
状态更新器传递给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
变化时都重新清理和设置间隔。
移除不必要的对象依赖项
如果你的 Effect 依赖于渲染过程中创建的对象或函数,它可能会运行过于频繁。例如,此 Effect 在每次渲染后都会重新连接,因为options
对象在每次渲染时都不同:
const serverUrl = 'https://127.0.0.1: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
// ...
避免使用渲染过程中创建的对象作为依赖项。而应该在 Effect 内部创建该对象。
import { useState, useEffect } from 'react'; import { createConnection } from './chat.js'; const serverUrl = 'https://127.0.0.1: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
字符串。
通过此修复,在输入框中输入内容不会重新连接聊天。与会被重新创建的对象不同,像roomId
这样的字符串不会改变,除非你将其设置为另一个值。阅读更多关于移除依赖项的信息。
移除不必要的函数依赖项
如果你的 Effect 依赖于渲染过程中创建的对象或函数,它可能会运行过于频繁。例如,此 Effect 在每次渲染后都会重新连接,因为createOptions
函数在每次渲染时都不同:
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
// ...
就其本身而言,在每次重新渲染时从头创建一个函数并不是问题。你不需要优化这一点。但是,如果你将其用作 Effect 的依赖项,它将导致你的 Effect 在每次重新渲染后重新运行。
避免使用渲染过程中创建的函数作为依赖项。而应该在 Effect 内部声明它。
import { useState, useEffect } from 'react'; import { createConnection } from './chat.js'; const serverUrl = 'https://127.0.0.1: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
这样的字符串不会改变,除非你将其设置为另一个值。阅读更多关于移除依赖项的信息。
从 Effect 中读取最新的 props 和状态
默认情况下,当你从 Effect 中读取响应式值时,你必须将其添加为依赖项。这确保你的 Effect 对该值的每次更改都“做出反应”。对于大多数依赖项来说,这就是你想要的行为。
但是,有时你可能想要从 Effect 中读取最新的 props 和状态,而无需对其“做出反应”。例如,假设你想要在每次页面访问时记录购物车中商品的数量。
function Page({ url, shoppingCart }) {
useEffect(() => {
logVisit(url, shoppingCart.length);
}, [url, shoppingCart]); // ✅ All dependencies declared
// ...
}
如果你想在每次url
更改后记录新的页面访问,但不是仅在shoppingCart
更改时记录呢? 你不能将shoppingCart
从依赖项中排除,否则会破坏响应式规则。 但是,你可以表达你不希望一段代码对更改“做出反应”,即使它是在 Effect 内部调用的。使用useEffectEvent
Hook声明一个Effect 事件,并将读取shoppingCart
的代码移到其中。
function Page({ url, shoppingCart }) {
const onVisit = useEffectEvent(visitedUrl => {
logVisit(visitedUrl, shoppingCart.length)
});
useEffect(() => {
onVisit(url);
}, [url]); // ✅ All dependencies declared
// ...
}
Effect 事件不是响应式的,并且必须始终从 Effect 的依赖项中省略。这就是让你可以在其中放置非响应式代码(你可以在其中读取某些 props 和状态的最新值)的原因。通过在onVisit
内部读取shoppingCart
,你可以确保shoppingCart
不会重新运行你的 Effect。
阅读更多关于 Effect 事件如何让你分离响应式和非响应式代码的信息。
在服务器和客户端显示不同的内容
如果你的应用使用服务器端渲染(无论是直接渲染还是通过框架),你的组件将在两个不同的环境中渲染。在服务器端,它将渲染以生成初始 HTML。在客户端,React 将再次运行渲染代码,以便它可以将你的事件处理程序附加到该 HTML。这就是为什么,为了使水合工作,你的初始渲染输出在客户端和服务器端必须相同。
在极少数情况下,你可能需要在客户端显示不同的内容。例如,如果你的应用从localStorage
读取一些数据,那么它不可能在服务器端这样做。以下是如何实现这一点的
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
。
谨慎使用此模式。请记住,连接速度慢的用户会看到初始内容相当长的时间——可能是几秒钟——因此你不想对组件的外观进行突然的更改。在许多情况下,你可以通过使用 CSS 有条件地显示不同的内容来避免这种需要。
故障排除
我的Effect在组件挂载时运行两次
当启用严格模式时,在开发环境中,React会在实际设置之前额外运行一次设置和清理。
这是一个压力测试,用于验证你的Effect的逻辑是否正确实现。如果这会导致可见问题,则你的清理函数缺少一些逻辑。清理函数应该停止或撤消设置函数正在执行的操作。经验法则是,用户不应该能够区分设置被调用一次(如在生产环境中)和设置→清理→设置序列(如在开发环境中)。
我的Effect在每次重新渲染后运行
首先,检查你是否忘记指定依赖项数组。
useEffect(() => {
// ...
}); // 🚩 No dependency array: re-runs after every render!
如果你已经指定了依赖项数组,但你的Effect仍然循环重新运行,那是因为你的某个依赖项在每次重新渲染时都不同。
你可以通过手动将依赖项记录到控制台来调试此问题。
useEffect(() => {
// ..
}, [serverUrl, roomId]);
console.log([serverUrl, roomId]);
然后,你可以右键单击控制台中来自不同重新渲染的数组,并为两者选择“存储为全局变量”。假设第一个保存为temp1
,第二个保存为temp2
,然后你可以使用浏览器控制台检查两个数组中的每个依赖项是否相同。
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 ...
当你找到每次重新渲染时都不同的依赖项时,通常可以通过以下几种方法之一来修复它。
作为最后手段(如果这些方法没有帮助),使用useMemo
或useCallback
(对于函数)包装其创建。
我的Effect陷入无限循环
如果你的Effect陷入无限循环,则以下两点必须为真:
- 你的Effect正在更新某些状态。
- 该状态导致重新渲染,这会导致Effect的依赖项发生变化。
在你开始修复问题之前,问问自己你的Effect是否连接到某个外部系统(例如DOM、网络、第三方小部件等)。为什么你的Effect需要设置状态?它是否与该外部系统同步?或者你是否试图用它来管理应用程序的数据流?
如果没有外部系统,请考虑完全移除Effect是否会简化你的逻辑。
如果你真的正在与某个外部系统同步,思考一下为什么以及在什么条件下你的 Effect 应该更新状态。是否有影响组件视觉输出的内容发生了变化?如果你需要跟踪一些渲染中未使用的數據,则 ref(不会触发重新渲染)可能更合适。请验证你的 Effect 不会过度更新状态(并触发重新渲染)。
最后,如果你的 Effect 在正确的时间更新状态,但仍然存在循环,那是因为该状态更新导致 Effect 的依赖项之一发生了变化。阅读如何调试依赖项更改。
我的清理逻辑即使我的组件没有卸载也会运行
清理函数不仅在卸载期间运行,而且在每次依赖项更改重新渲染之前都会运行。此外,在开发过程中,React 会在组件挂载后立即额外运行一次设置+清理。
如果你有清理代码但没有相应的设置代码,这通常是一种代码异味。
useEffect(() => {
// 🔴 Avoid: Cleanup logic without corresponding setup logic
return () => {
doSomething();
};
}, []);
你的清理逻辑应该与设置逻辑“对称”,并且应该停止或撤销设置所做的任何操作。
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [serverUrl, roomId]);
我的 Effect 做了一些视觉上的操作,我看到它运行之前闪烁了一下
如果你的 Effect 必须阻止浏览器绘制屏幕,请将 useEffect
替换为 useLayoutEffect
。请注意,对于绝大多数 Effect,这都不需要。只有在必须在浏览器绘制之前运行你的 Effect 时才需要这样做:例如,在用户看到它之前测量和定位工具提示。