本专栏致力于每周分享一个项目,如果本文对你有帮助的话,欢迎点赞或者关注☘️
React18 源码系列会随着学习 React 源码的实时进度而实时更新:约,两天一小改,五天一大改。
useEffect基本使用回顾
UseEffect是什么
useEffect是一个 React Hook,可让您将组件与外部系统同步。例如,您可能希望根据 React 状态控制非 React 组件、设置服务器连接或在组件出现在屏幕上时发送分析日志。Effects允许您在渲染后运行一些代码,以便您可以将组件与 React 之外的某些系统同步。
UseEffect语法
语法:useEffect(setup, dependencies?) // 在组件的顶层调用useEffect来声明 Effect
- setup: 具有Effect逻辑的函数。您的设置函数还可以选择返回清理函数。当你的组件被添加到 DOM 中时,React 将运行你的设置函数。每次使用更改的依赖项重新渲染后,React 将首先使用旧值运行清理函数(如果您提供了它),然后使用新值运行设置函数。从 DOM 中删除组件后,React 将运行您的清理函数。
- dependencies : setup代码中引用的所有reactive值的列表。reactive值包括 props、state 以及直接在组件体内声明的所有变量和函数。如果您的 linter配置为 React ,它将验证每个reactive值是否已正确指定为依赖项。依赖项列表必须具有恒定数量的项目,并且像
[dep1, dep2, dep3]
那样内联编写。 React 将使用[Object.is](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is)
比较将每个依赖项与其先前的值进行比较。如果省略此参数,您的 Effect 将在每次重新渲染组件后重新运行 - return: useEffect返回undefined
UseEffect使用的注意事项
- useEffect是一个 Hook,因此您只能在组件的顶层或您自己的 Hook 中调用它。您不能在循环或条件内调用它。如果需要,请提取一个新组件并将状态移入其中。
- 如果您不尝试与某些外部系统同步, 则可能不需要Effect
- 当严格模式打开时,React 将在第一次真正设置之前运行一个额外的仅开发设置+清理周期。这是一个压力测试,可确保您的清理逻辑“镜像”您的设置逻辑,并确保它停止或撤消设置正在执行的任何操作。如果这导致问题,请实施清理功能。
- 如果您的某些依赖项是在组件内部定义的对象或函数,则存在它们会导致 Effect 重新运行频率超过所需频率的风险。 要解决此问题,请删除不必要的对象和函数依赖项。您还可以在 Effect 之外提取状态更新和非 reactive 逻辑。
- 如果您的 Effect 不是由交互(如点击)引起的,React 通常会让浏览器在运行您的 Effect 之前先绘制更新的屏幕。 如果您的 Effect 正在执行一些视觉操作(例如,定位工具提示),并且延迟很明显(例如,闪烁),请将useEffect替换为useLayoutEffect 。
- 如果您的 Effect 是由交互(如点击)引起的, React 可能会在浏览器绘制更新的屏幕之前运行您的 Effect 。这保证了Effect的结果可以被事件系统观察到。通常,这会按预期工作。但是,如果您必须将工作推迟到绘制之后(例如alert() ,则可以使用setTimeout 。
- 即使您的 Effect 是由交互(例如单击)引起的, React 也可能允许浏览器在处理 Effect 内的状态更新之前重新绘制屏幕。 通常,这会按预期工作。但是,如果必须阻止浏览器重新绘制屏幕,则需要将useEffect替换为useLayoutEffect 。
- Effect仅在客户端上运行。 它们在服务器渲染期间不会运行。
Useeffect用法
连接到外部系统
某些组件在显示在页面上时需要保持与网络、某些浏览器 API 或第三方库的连接。这些系统不受 React 控制,因此称为外部系统。**如果您没有连接到任何外部系统,则可能不需要效果器。 **外部系统是指任何不受React控制的代码,例如:
- 使用
[setInterval()](https://developer.mozilla.org/en-US/docs/Web/API/setInterval)
和[clearInterval()](https://developer.mozilla.org/en-US/docs/Web/API/clearInterval)
管理的计时器。 - 用
[window.addEventListener()](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener)
和[window.removeEventListener()](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener)
进行事件订阅。通常,您可以使用 JSX 指定事件侦听器,但不能以这种方式侦听全局[window](https://developer.mozilla.org/en-US/docs/Web/API/Window)
对象。 Effect 允许您连接到window
对象并监听其事件。 - 具有animation.start()和animation.reset()等 API 的第三方动画库。
要将组件连接到某个外部系统,请在组件的顶层调用useEffect :
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]);
// ...
}
React 在必要时调用您的设置和清理函数,这可能会发生多次:
- 当您的组件添加到页面 (mount) 时,您的设置代码就会运行。
- 每次重新渲染依赖项发生变化的组件后:
- 首先,您的清理代码使用旧的 props 和state 运行。
- 然后,您的设置代码将使用新的 props 和 state 运行。
- 从页面中删除(卸载)组件后,您的清理代码将最后运行一次 。
自定义 Hook 中的 Wrapping Effects
Effect 是一个“逃生舱口(escape hatch)”:当您需要“走出 React”并且没有更好的内置解决方案适合您的用例时,您可以使用它们。如果您发现自己经常需要手动编写 Effects,这通常表明您需要为组件所依赖的常见行为提取一些自定义 Hook 。
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://localhost:1234');
useChatRoom({
roomId: roomId,
serverUrl: serverUrl
});
// ...
控制非 React 小部件
有时,您希望外部系统与组件的某些属性或状态保持同步。
例如,如果您有一个没有使用 React 编写的第三方地图小部件或视频播放器组件,您可以使用 Effect 来调用其上的方法,使其状态与 React 组件的当前状态匹配。
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 引擎自动进行垃圾收集。
使用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]);
// ...
使用[async](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function)
/ [await](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_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>
</>
);
}
指定反应性依赖项
Effect 代码使用的每个反应值都必须声明为依赖项。你的 Effect 的依赖列表是由周围的代码决定的.反应性值包括 props 以及直接在组件内部声明的所有变量和函数。
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
// ...
}
当任何组件的 props 或状态更改时,具有空依赖项的 Effect不会重新运行。
传递反应性依赖项的示例
- 传递依赖数组
如果您指定依赖项,您的 Effect 将在初始渲染后以及使用更改的依赖项重新渲染后运行。
- 传递一个空的依赖数组
如果您的效果确实不使用任何反应值,它只会在初始渲染后运行
- 如果您根本不传递任何依赖项数组,则您的 Effect 将在组件的每次渲染(和重新渲染)后运行。
根据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
是一个反应值,因此必须在依赖项列表中指定它。但是,这会导致每次count
更改时Effect都会重新清理和设置。这并不理想。
要解决此问题,请将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更改时都不需要再次清理和设置interval。
删除不必要的对象依赖项
如果您的 Effect 依赖于渲染期间创建的对象或函数,则它可能运行得太频繁。例如,此 Effect 在每次渲染后重新连接,因为每次渲染的options
对象都不同:
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
// ...
避免使用渲染期间创建的对象作为依赖项。相反,在 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 依赖于渲染期间创建的对象或函数,则它可能运行得太频繁。例如,此 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://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 中读取最新的 props 和状态
默认情况下,当您从 Effect 读取响应值时,必须将其添加为依赖项。这可确保您的Effect对该值的每次变化做出“反应”。对于大多数依赖项,这就是您想要的行为。
然而,有时您会想要从 Effect 中读取最新的props 和状态,而不会对它们做出“反应”。 例如,假设您想要记录每次页面访问的购物车中的商品数量:
function Page({ url, shoppingCart }) {
useEffect(() => {
logVisit(url, shoppingCart.length);
}, [url, shoppingCart]); // ✅ All dependencies declared
// ...
}
如果您想在每次url
更改后记录新的页面访问,但如果仅shoppingCart
发生更改,该怎么办? 在不违反反应性规则的情况下,您无法从依赖项中排除shoppingCart
但是,您可以表示您不希望一段代码对更改做出“反应”,即使它是从 Effect 内部调用的。使用[useEffectEvent](https://react.dev/reference/react/experimental_useEffectEvent)
Hook声明一个Effect Event ,并将读取shoppingCart
代码移动到其中:
Effect Events 不是反应性的,必须始终从 Effect 的依赖项中省略。
这可以让您将非反应性代码(您可以在其中读取某些 props 和 state 的最新值)放入其中。通过读取onVisit
内部的shoppingCart
,您可以确保shoppingCart
不会重新运行您的 Effect。
在服务器和客户端上显示不同的内容
如果您的应用程序使用服务器渲染(直接或通过框架),您的组件将在两个不同的环境中渲染。在服务器上,它将呈现以生成初始 HTML。在客户端,React 将再次运行渲染代码,以便将您的事件处理程序附加到该 HTML。这就是为什么要使水合作用,客户端和服务器上的初始渲染输出必须相同。
在极少数情况下,您可能需要在客户端上显示不同的内容。例如,如果您的应用程序从[localStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/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学习资源:
- react.dev/reference/r…
关于作者
作者:Wandra
内容:算法 | 趋势 |源码|Vue | React | CSS | Typescript | Webpack | Vite | GithubAction | GraphQL | Uniqpp。
专栏:欢迎关注呀🌹
本专栏致力于每周分享一个项目,如果本文对你有帮助的话,欢迎点赞或者关注☘️