useEffect
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
:带有效果逻辑的函数。setup 函数还可以选择返回一个清理函数。当组件添加到 DOM 时,React 将运行 setup 函数。每次在更改依赖项后重新渲染时,React 会先使用旧值运行清理函数(如果已提供),然后使用新值运行 setup 函数。当组件从 DOM 中删除后,React 将运行清理函数。 -
可选
dependencies
:在setup
代码中引用所有响应值列表。响应值包括道具、状态以及直接在组件体中声明的所有变量和函数。如果代码检查程序已 为 React 配置,它将验证每个响应值是否正确指定为依赖项。依赖项列表必须包含固定数量的项并按[dep1, dep2, dep3]
的形式内联编写。React 将使用Object.is
比较将每个依赖项与其上一个值进行比较。如果你省略此参数,你的 Effect 将在组件每次重新渲染后重新运行。 请查看传递依赖项数组、空数组以及不传递任何依赖项之间的差异。
返回
useEffect
返回 undefined
。
注意事项
-
useEffect
为 Hook,因此您只能在 **组件的最顶层** 或您自己的 Hooks 中调用它。您不能在循环或条件内部调用它。如果您需要此功能,请提取一个新组件并将其中的 state 移入其中。 -
如果您不是 **试图与某个外部系统同步**,您可能无需 Effect。
-
开启严格模式后,React 会在首次实际设置之前 **运行一个额外的仅开发设置+清除周期**。这是一次压力测试,用以确保清除逻辑“镜像”设置逻辑并且它停止或撤消设置所执行的操作。如果这引起了问题,请 实现清除函数。
-
如果某些依赖项是在组件内定义的对象或函数,则它们可能会 导致效果比需要的情况重跑得更频繁。要修复此问题,请删除不必要的 对象 和 函数 依赖项。您还可以 提取状态更新 和 非响应式逻辑,放在效果之外。
-
如果效果不是由交互(如点击)引起的,React 通常会让浏览器 先绘制更新的屏幕,然后再运行您的效果。如果你的效果正在执行一些视觉上的东西(例如,定位提示框),并且延迟很明显(例如,闪烁),用
useLayoutEffect
替换useEffect
。 -
如果你的效果是由交互行为引起的(例如点击),React 会在浏览器绘制更新的屏幕之前运行你的效果。这将确保事件系统可以观察到效果的结果。通常,这种情况会按预期工作。但是,如果你必须将这项工作推迟到绘制之后,比如
alert()
,你可以使用setTimeout
。请参阅 reactwg/react-18/128 以获取更多信息。 -
即使你的效果是由交互行为引起的(例如点击),React 会允许浏览器在你效果里的状态更新处理之前重新绘制屏幕。通常,这种情况会按预期工作。但是,如果你必须阻止浏览器重新绘制屏幕,你需要用
useLayoutEffect
替换useEffect
。 -
效果 仅在客户端上运行。它们不会在服务器渲染期间运行。
使用
连接到外部系统
当一些组件在页面上展示时,它们需要与网络、浏览器 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 会在每次必要时调用您的设置函数和清理函数,可能多次调用
- 當您的元件加入到頁面時,您的設定程式碼會執行(掛載)。
- 在您的元件每次重複繪製,而相依元件已經改變時
- 首先,您的清除程式碼會使用舊的屬性和狀態執行。
- 然後,您的設定程式碼會使用新的屬性和狀態執行。
- 在您的元件從頁面移除後,您的清除程式碼會最後執行一次(解除掛載)。
讓我們為上述範例說明此順序。
當上方的ChatRoom
元件加入頁面時,它會連線到聊天室,並使用初始serverUrl
以及roomId
。如果serverUrl
或roomId
因為重新繪製而有改變(例如:使用者在下拉式選單中挑選另一個聊天室),您的效果會與前一個房間斷線,並連線至下一個房間。當ChatRoom
元件從頁面上移除時,您的效果會最後斷線一次。
为了帮助你发现 bug,在开发环境中,React 在setup之前多运行一次cleanup。这是一个压力测试,用于验证你的 Effect 的逻辑是否正确实现。如果这导致明显的问题,则你的 cleanup 函数缺少一些逻辑。cleanup 函数应该停止或撤消 setup 函数所做的任何事情。经验法则是,用户不应该能够区分作为一次调用的 setup(如在生产环境中)和setup → cleanup → setup 序列(如在开发环境中)。参见通用解决方案。
尝试将每个 Effect 写为一个独立的进程并一次考虑一个 setup/cleanup 周期。你的组件是装入、更新还是卸载都不必是重要的。当你的 cleanup 逻辑正确地“镜像”了 setup 逻辑时,你的 Effect 能够根据需要频繁地运行 setup 和 cleanup。
示例 1of 5: 连接到聊天服务器
在此示例中,ChatRoom
组件使用一种效果保持与 chat.js
中定义的外部系统连接。点击“打开聊天”以使 ChatRoom
组件显示。此沙盒以开发模式运行,因此有一个额外的连接和断开连接周期,正如在此处所解释。尝试使用下拉列表和输入更改 roomId
和 serverUrl
,看看该效果如何重新连接到聊天。点击“关闭聊天”以查看该效果最后一次断开连接。
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,则通常表明你需要提取一些自定义 Hooks,以便组件依赖于你的常见行为。
例如,此useChatRoom
自定义 Hook 在更具声明性的 API 后面“隐藏”了你的 Effect 的逻辑
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 生态系统中还有很多针对每种用途的出色的自定义 Hooks。
示例 1of 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 编写的第三方地图小部件或视频播放器组件,你可以使用一个效果来调用其上的方法,使它的状态匹配你 React 组件的当前状态。此效果创建一个 MapWidget
类的实例,该类定义在 map-widget.js
中。当你更改 Map
组件的 zoomLevel
prop 时,该效果会调用该类实例上的 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 节点。在从 tree 中移除 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 更容易,它可以是你自己的,也可以由社区维护。
深入了解
Effect 中编写获取
调用是一种获取数据的一种流行方法,这种方法在完全客户端应用程序中尤其流行。然而,这是一个非常手动的做法,并且有很多缺点
- Effect 在服务器上不会运行。这意味着服务器最初呈现的 HTML 只包含一个加载状态,不包含数据。客户端计算机将不得不下载所有 JavaScript 并呈现你的应用程序,最后却发现它现在需要加载数据。这种方式的效率非常低。
- 在 Effect 中直接获取容易形成“网络瀑布”。呈现父组件,获取一些数据,呈现子组件,然后子组件开始获取它们的数据。如果网络不是非常快,这会比并行获取所有数据慢得多。
- 在 Effects 中直接获取通常意味着你不预加载或缓存数据。例如,如果组件卸载然后再次加载,它会再次获取数据。
- 这并非十分便利。在编写
fetch
调用时会涉及相当多的样板代码,而这种代码并不会出现诸如 竞争条件 等问题。
此缺点列表并非 React 所特有。它适用于使用任何库在 mount 时获取数据的情况。与路由一样,数据获取并非易事,因此我们推荐以下方法
- 如果您使用 框架,请使用其内置的数据获取机制。现代 React 框架集成了高效且不会出现上述缺点的数据获取机制。
- 否则,请考虑使用或构建客户端缓存。流行的开源解决方案包括 React Query、useSWR 及 React Router 6.4+。您还可以构建自己的解决方案,在这种情况下,您将在基础中使用 Effects,但还要添加用于消除重复请求、缓存响应及避免网络瀑布的逻辑(通过预加载数据或将数据要求提升至路由)。
如果您不适合这些方法,则可以继续在 Effects 中直接获取数据。
指定 reactive 依赖
注意,你无法 "选择" Effect 的依赖项。Effect 的代码使用的每个 reactive 值 都必须声明为一个依赖项。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 就会使用新值重新连接到聊天
Reactive 值 包括 prop 以及在你组件内直接声明的所有变量和函数。由于 roomId
和 serverUrl
是 reactive 值,你无法将它们从依赖项中移除。如果你尝试忽略它们,并且 已为 React 正确配置了 linter, 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 或状态更改时都不会重新运行。
示例 1of 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}/>} </> ); }
基于来自效果的前一状态更新状态
当你想从效果中基于前一状态更新状态时,你可能遇到一个问题
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
改变时都会清理并重新设置。这不是理想的。
要修复此问题,将 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 中调用。声明一个 Effect Event(效应事件),使用 useEffectEvent
Hook,然后将读取 shoppingCart
的代码移至其中
function Page({ url, shoppingCart }) {
const onVisit = useEffectEvent(visitedUrl => {
logVisit(visitedUrl, shoppingCart.length)
});
useEffect(() => {
onVisit(url);
}, [url]); // ✅ All dependencies declared
// ...
}
Effect Event(效应事件)没有反应性且必须始终从您的 Effect 的依赖项中省略。 这就是您可以将非反应性代码(您可以从中读取某些属性和状态的最新值)放置在其中。onVisit
中通过读取 shoppingCart
,您可以确保 shoppingCart
不会重新运行您的 Effect。
详细了解 Effect Event(效应事件)如何使您能够分离反应性和非反应性代码。
在服务器和客户端上显示不同内容
如果你的应用使用了服务端渲染(直接使用 直接 使用或使用 框架),你的组件将会在两个不同的环境中渲染。在服务端,它将会渲染生成初始 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 有条件地显示不同内容来避免出现此需求。
故障排除
组件加载时,我的效果运行了两次
在开发模式下,当启用严格模式时,React 会在实际设置之前额外运行一次设置和清理。
这是一个压力测试,用于验证您效果的逻辑是否已正确实现。如果这导致出现明显的问题,表明您的清理功能缺少一些逻辑。清理功能应停止或撤消设置功能执行的动作。经验法则是,用户不应该能够区分调用设置一次(如在生产中)和设置 → 清理 → 设置序列(如在开发中)
阅读更多与此帮助查找错误和如何修复你的代码逻辑问题。有关的信息。
我的副作用在每次重新渲染之后都运行
首先,检查你是否忘记指定依赖项数组
useEffect(() => {
// ...
}); // 🚩 No dependency array: re-runs after every render!
如果你已指定依赖项数组,但副作用仍会循环重新运行,则可能是因为每次重新渲染时,某个依赖项不同。
你可以通过手动记录依赖项到控制台来调试此问题
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
。请注意,对于绝大多数的效果而言,这是没有必要的。只有在浏览器绘制前必须运行你的效果时,才需要这样做:例如,在用户看到工具提示前测量和定位工具提示。