Effect 与副作用
useEffect 用于在组件渲染后执行与外部系统同步的逻辑(请求、订阅、手动改 DOM 等)。见 State。
核心概念
useEffect 解决的问题是:「组件渲染出来之后,我需要让某个外部系统(DOM、网络、第三方库、浏览器 API)与当前的 props/state 保持同步」。它不是「在某个时机运行一段代码」的万能钩子,而是专门为同步这件事设计的——理解这一点,才能判断什么时候该用 Effect,什么时候不该用。
基本用法
import { useEffect, useState } from "react";
function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
let cancelled = false;
fetch(`/api/users/${userId}`)
.then((r) => r.json())
.then((data) => {
if (!cancelled) setUser(data);
});
return () => {
cancelled = true;
};
}, [userId]);
if (!user) return <p>加载中…</p>;
return <p>{user.name}</p>;
}
| 部分 | 说明 |
|---|---|
依赖数组 [userId] | 仅当 userId 变化时重新执行 effect |
清理函数 return () => {...} | 下次 effect 执行前或组件卸载时调用,用于取消订阅、忽略过期请求 |
依赖数组
| 写法 | 行为 |
|---|---|
| 省略第二参数 | 每次渲染后都执行(慎用) |
[] | 仅挂载后执行一次(注意 StrictMode 开发下可能执行两次) |
[a, b] | a 或 b 变化时执行 |
依赖须包含 effect 内读取的所有响应式值(props、state、组件内变量),否则可能读到陈旧闭包。由 ESLint react-hooks/exhaustive-deps 辅助检查。
示例:依赖遗漏导致的陈旧闭包
function SearchBox() {
const [query, setQuery] = useState("");
const [debounced, setDebounced] = useState("");
useEffect(() => {
const timer = setTimeout(() => {
setDebounced(query); // 若依赖数组漏写 query,这里永远是首次渲染时的空字符串
}, 300);
return () => clearTimeout(timer);
}, [query]); // 正确:包含 query
return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
}
订阅外部事件的完整示例
function useWindowWidth() {
const [width, setWidth] = useState(() => window.innerWidth);
useEffect(() => {
function handleResize() {
setWidth(window.innerWidth);
}
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize); // 必须清理,否则组件卸载后仍在监听
}, []);
return width;
}
resize 事件、WebSocket 连接、第三方库实例(地图、图表)都遵循同一个模式:在 Effect 中建立连接,在清理函数中断开。
Effect 适用场景
适合:连接外部系统、订阅、非 React 控制的 DOM 操作。
不适合(优先考虑其他方式):
- 由事件触发的业务逻辑 → 写在事件处理函数中。
- 派生数据 → 用
useMemo或在渲染时直接计算。 - 服务端数据缓存 → 考虑 TanStack Query 等。
反例:本该在事件处理函数里的逻辑
// 反例:用 Effect 模拟「点击后跳转」,多了一次不必要的渲染,且逻辑意图不直观
function Form() {
const [submitted, setSubmitted] = useState(false);
useEffect(() => {
if (submitted) {
navigate("/success");
}
}, [submitted]);
return <button onClick={() => setSubmitted(true)}>提交</button>;
}
// 正确:直接在事件处理函数中处理
function Form() {
function handleSubmit() {
navigate("/success");
}
return <button onClick={handleSubmit}>提交</button>;
}
「由用户操作直接触发的后续动作」应该写在事件处理函数里,而不是通过一个 state 变量间接触发 Effect——后者多一次渲染,且难以看出因果关系。
useLayoutEffect
在浏览器绘制前同步执行,用于读取布局并同步更新 DOM,避免闪烁。一般优先 useEffect,仅在测量 DOM 后需立即改样式时使用 useLayoutEffect:
function Tooltip({ targetRef }: { targetRef: RefObject<HTMLElement> }) {
const [position, setPosition] = useState({ top: 0, left: 0 });
const tooltipRef = useRef<HTMLDivElement>(null);
useLayoutEffect(() => {
const rect = targetRef.current!.getBoundingClientRect();
// 在浏览器绘制前完成测量与定位,避免用户看到 tooltip 先出现在错误位置再跳到正确位置
setPosition({ top: rect.bottom, left: rect.left });
}, [targetRef]);
return <div ref={tooltipRef} style={{ position: "absolute", ...position }}>提示内容</div>;
}
最佳实践
- 写 Effect 前先问:「这是不是在同步外部系统?」如果答案是「不是」,大概率不该用 Effect。
- 依赖数组交给 ESLint
react-hooks/exhaustive-deps检查,不要手动省略依赖来"控制"执行时机——应该通过重构代码结构来控制。 - 网络请求类 Effect 一律处理竞态(取消/忽略过期响应),否则快速切换
userId等参数时可能显示错误的数据。 - 订阅/连接类 Effect 必须返回清理函数,对称地建立与销毁。
- 复杂的数据获取逻辑迁移到专门的库(TanStack Query 等),而非手写一堂
useEffect+useState处理 loading/error/data 三态。
常见坑
| 现象 | 常见原因 | 处理 |
|---|---|---|
| 切换参数后短暂显示旧/错误数据 | 未处理请求竞态(旧请求晚于新请求返回) | 用 cancelled 标志位或 AbortController 忽略过期结果 |
| 组件卸载后报「在已卸载组件上更新状态」警告 | 未清理订阅/定时器/请求回调 | 清理函数中取消订阅,或用取消标志位 |
| Effect 无限循环执行 | 依赖数组包含每次渲染都新建的对象/函数 | 用 useMemo/useCallback 稳定依赖,或调整依赖粒度 |
| ESLint 提示缺依赖但加上后逻辑「变了」 | 加上依赖后 Effect 执行时机变化,暴露了原有设计问题 | 重构代码而非禁用 lint 规则 |
| 开发环境下 Effect 执行了两次 | StrictMode 故意双重调用以检测副作用问题 | 确保 Effect 具备正确的清理逻辑,而不是抑制该行为 |
延伸阅读
参考文献
以下链接在编写时均可正常访问:
| 资料 | 说明 |
|---|---|
| React:useEffect | API |
| React:同步 Effect | 使用指南 |
| React:useLayoutEffect | 布局 effect |
| React:你可能不需要 Effect | 反模式清单 |
相关文章
Context
Context 在组件树中跨层级传递数据,避免 props 逐层透传。涵盖创建与使用、自定义 Hook 封装、性能拆分、与全局状态库对照、最佳实践与常见坑。
Memo、Callback 与 Ref
本文介绍 useRef、useMemo、useCallback、memo 及自定义 Hook。见 Effect。
State 与 Reducer
Hooks 让函数组件拥有 state 与副作用能力。本文介绍 useState 与 useReducer。见 React 基础。
渲染与协调
React 将组件渲染为虚拟 DOM 树,再协调(reconciliation)到浏览器 DOM。理解该过程有助于解释 key、memo 与性能优化。涵盖渲染流程、Fiber、startTransition、Suspense、最佳实践与常见坑。
工程化
新建 React 项目时,官方与社区主流工具为 Vite;全栈场景常用 Next.js。涵盖 Vite + React、Create React App 现状、Next.js、目录结构、环境变量、部署、最佳实践与常见坑。
TypeScript
React 官方推荐 TypeScript。类型主要落在 Props、事件、Ref 与 Hooks 返回值上。见 组件。
Series
hooks
2 / 4