
React 学习笔记:Effect Hook
发布于: 2026-06-05 13:19:19 更新于: 2026-06-05 13:19:19
useEffect 基础
什么是副作用(Side Effect)?
在 React 中,组件的主要职责是根据 state 和 props 渲染 UI。但实际应用中,组件还需要做一些"额外的事情":
- 发送网络请求(获取数据)
- 操作 DOM(修改标题、聚焦输入框)
- 设置定时器
- 订阅外部数据源(WebSocket、事件监听)
- 打印日志
这些"额外的事情"统称为副作用(Side Effect)。useEffect 就是 React 提供的、用于在函数组件中执行副作用的 Hook。
基本用法
import { useEffect } from "react";
function MyComponent() {
// 在每次渲染后执行
useEffect(() => {
document.title = "页面已更新";
});
return <div>...</div>;
}
关键点:
- 在组件顶层调用
useEffect useEffect接收一个函数(称为 "Effect"),React 会在渲染并提交到屏幕后执行它- Effect 函数可以返回一个清理函数(可选),React 会在下次执行 Effect 前或组件卸载时调用它
语法结构
useEffect(() => {
// 🔵 执行副作用(mount / update 时运行)
return () => {
// 🟡 清理副作用(下次 Effect 运行前或 unmount 时运行)
};
}, [dependencies]); // 依赖数组
依赖数组(Dependency Array)
依赖数组是 useEffect 的第二个参数,它决定了 Effect 何时重新执行。
三种形式
// 1. 不传依赖数组 → 每次渲染后都执行
useEffect(() => {
console.log("每次渲染后都会执行");
});
// 2. 空数组 [] → 只在组件挂载时执行一次
useEffect(() => {
console.log("仅在挂载时执行一次");
}, []);
// 3. 指定依赖 [a, b] → 挂载时执行,且 a 或 b 变化时重新执行
useEffect(() => {
console.log("挂载时执行,count 或 name 变化时也会执行");
}, [count, name]);
依赖数组对照表
| 依赖数组 | 执行时机 | 等价于(类组件) |
|---|---|---|
| 不传 | 每次渲染后 | componentDidMount + componentDidUpdate |
[] |
仅挂载时 | componentDidMount |
[a, b] |
挂载时 + a 或 b 变化时 | componentDidMount + 部分 componentDidUpdate |
依赖数组的工作原理
React 使用 Object.is 逐个比较依赖数组中的值:
const [count, setCount] = useState(0);
const [name, setName] = useState("Alice");
useEffect(() => {
console.log("Effect 执行了");
// 依赖 [count, name]
// React 会比较:Object.is(prevCount, newCount) && Object.is(prevName, newName)
// 如果有任一不同,就重新执行 Effect
}, [count, name]);
清理函数(Cleanup Function)
有些副作用需要在结束时"清理",比如取消订阅、清除定时器。Effect 可以返回一个清理函数:
基本示例
useEffect(() => {
const timer = setInterval(() => {
console.log("每秒执行");
}, 1000);
// 清理函数:组件卸载时清除定时器
return () => {
clearInterval(timer);
};
}, []);
执行顺序
组件挂载 → 执行 Effect
↓
组件更新 → 执行上一次的清理函数 → 执行新的 Effect
↓
组件卸载 → 执行清理函数
function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(roomId);
connection.connect();
return () => {
connection.disconnect(); // 切换 roomId 时,先断开旧连接
};
}, [roomId]);
// ...
}
重要: 清理函数捕获的是上一次渲染的 props 和 state(闭包特性),而不是最新的值。
常见使用场景
1. 操作 DOM(修改页面标题)
function PageTitle({ title }) {
useEffect(() => {
document.title = title;
}, [title]); // title 变化时更新
return <h1>{title}</h1>;
}
2. 添加事件监听
function WindowSize() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
function handleResize() {
setWidth(window.innerWidth);
}
window.addEventListener("resize", handleResize);
// 清理:移除事件监听
return () => {
window.removeEventListener("resize", handleResize);
};
}, []); // 只在挂载时添加一次
return <p>窗口宽度: {width}px</p>;
}
3. 设置定时器
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds((prev) => prev + 1);
}, 1000);
// 清理:组件卸载时停止定时器
return () => clearInterval(interval);
}, []);
return <p>已过去 {seconds} 秒</p>;
}
4. 获取网络请求数据
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let ignore = false; // 用于防止竞态条件
setLoading(true);
async function fetchUser() {
try {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
// 只在 ignore 为 false 时更新状态
// (防止旧请求的结果覆盖新请求的结果)
if (!ignore) {
setUser(data);
setLoading(false);
}
} catch (error) {
if (!ignore) {
setLoading(false);
}
}
}
fetchUser();
// 清理函数:标记该次请求已过期
return () => {
ignore = true;
};
}, [userId]); // userId 变化时重新获取
if (loading) return <p>加载中...</p>;
return <p>用户名: {user?.name}</p>;
}
注意 ignore 标志的作用: 当 userId 快速从 A → B 变化时,如果请求 A 的响应晚于请求 B,直接设置会导致数据错乱。ignore 标志确保旧请求的结果不会覆盖新请求。
5. 同步外部数据源
function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(roomId);
connection.connect();
// 清理:断开连接
return () => {
connection.disconnect();
};
}, [roomId]);
return <h1>欢迎来到 {roomId} 房间</h1>;
}
6. 滚动位置跟踪
function ScrollTracker() {
const [scrollY, setScrollY] = useState(0);
useEffect(() => {
function handleScroll() {
setScrollY(window.scrollY);
}
window.addEventListener("scroll", handleScroll);
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, []);
return <p>滚动位置: {scrollY}px</p>;
}
常见错误与陷阱
1. 无限循环
// ❌ 错误:没有依赖数组,Effect 中设置 state 会导致无限循环
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1); // 每次渲染后执行 → 设置 state → 重新渲染 → 再次执行...
});
修复方法: 添加正确的依赖数组,或使用事件处理函数代替 Effect。
// ✅ 正确:只在挂载时执行一次
useEffect(() => {
setCount(1);
}, []);
// ✅ 正确:依赖变化时才执行
useEffect(() => {
setCount(someValue + 1);
}, [someValue]);
2. 忘记清理副作用
// ❌ 错误:每次渲染都添加新的事件监听,且从不移除
useEffect(() => {
window.addEventListener("resize", handleResize);
});
// ✅ 正确:返回清理函数
useEffect(() => {
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
}, []);
3. 依赖数组遗漏
// ❌ 错误:Effect 中使用了 count,但依赖数组为空
useEffect(() => {
const id = setInterval(() => {
console.log(count); // 永远打印初始值 0
}, 1000);
return () => clearInterval(id);
}, []);
// ✅ 正确:将 count 加入依赖
useEffect(() => {
const id = setInterval(() => {
console.log(count); // 能读取到最新的 count
}, 1000);
return () => clearInterval(id);
}, [count]);
提示: 使用 ESLint 的 exhaustive-deps 规则可以自动检测遗漏的依赖。
4. Effect 中直接使用 async 函数
// ❌ 错误:useEffect 的回调不能直接是 async 函数
// async 函数返回 Promise,而 useEffect 期望返回清理函数或 undefined
useEffect(async () => {
const data = await fetchData();
setData(data);
}, []);
// ✅ 正确:在 Effect 内部定义 async 函数并调用
useEffect(() => {
async function fetchData() {
const data = await fetch("/api/data");
const json = await data.json();
setData(json);
}
fetchData();
}, []);
useEffect vs 事件处理函数
并非所有副作用都需要放在 useEffect 中。区分两者的使用场景很重要:
| 场景 | 使用事件处理函数 | 使用 useEffect |
|---|---|---|
| 用户点击按钮后发送请求 | ✅ | ❌ |
| 输入框内容变化后更新 | ✅ | ❌ |
| 组件挂载时连接聊天室 | ❌ | ✅ |
| 依赖变化时同步外部数据 | ❌ | ✅ |
| 每次渲染后更新 document.title | ❌ | ✅ |
function Form() {
const [name, setName] = useState("");
// ✅ 事件处理函数:用户主动触发
function handleSubmit() {
fetch("/api/submit", { method: "POST", body: name });
}
// ✅ useEffect:组件需要与外部系统同步
useEffect(() => {
document.title = `${name} - 编辑中`;
}, [name]);
return (
<form onSubmit={handleSubmit}>
<input value={name} onChange={(e) => setName(e.target.value)} />
</form>
);
}
自定义 Hook 中的 useEffect
useEffect 最强大的用法之一是在自定义 Hook 中封装可复用的副作用逻辑:
自定义 Hook:useWindowSize
function useWindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight,
});
useEffect(() => {
function handleResize() {
setSize({
width: window.innerWidth,
height: window.innerHeight,
});
}
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
return size;
}
// 使用
function App() {
const { width, height } = useWindowSize();
return (
<p>
窗口尺寸: {width} x {height}
</p>
);
}
自定义 Hook:useDebounce
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
// 使用
function SearchInput() {
const [query, setQuery] = useState("");
const debouncedQuery = useDebounce(query, 300);
useEffect(() => {
if (debouncedQuery) {
// 执行搜索请求
console.log("搜索:", debouncedQuery);
}
}, [debouncedQuery]);
return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
}
自定义 Hook:usePrevious
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
// 使用
function Counter() {
const [count, setCount] = useState(0);
const prevCount = usePrevious(count);
return (
<div>
<p>
当前: {count}, 上一次: {prevCount}
</p>
<button onClick={() => setCount((c) => c + 1)}>增加</button>
</div>
);
}
与类组件生命周期的对比
| 生命周期 | useEffect 等价写法 |
|---|---|
componentDidMount |
useEffect(() => { ... }, []) |
componentDidUpdate |
useEffect(() => { ... }) 或带依赖的 useEffect |
componentWillUnmount |
useEffect(() => { return () => { ... } }, []) |
// 类组件
class Timer extends React.Component {
state = { seconds: 0 };
componentDidMount() {
this.interval = setInterval(() => {
this.setState((s) => ({ seconds: s.seconds + 1 }));
}, 1000);
}
componentWillUnmount() {
clearInterval(this.interval);
}
render() {
return <p>{this.state.seconds}</p>;
}
}
// 等价的函数组件写法
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds((s) => s + 1);
}, 1000);
return () => clearInterval(interval);
}, []);
return <p>{seconds}</p>;
}
重要注意事项
1. Effect 在渲染后执行
useEffect 中的代码在组件渲染到屏幕之后才执行,不会阻塞浏览器绘制。这意味着用户会先看到更新后的 UI,然后 Effect 才运行。
function App() {
useEffect(() => {
// 这里的代码在组件渲染到屏幕后执行
console.log("组件已渲染到屏幕");
});
return <div>内容</div>;
// 输出顺序:先渲染"内容"到屏幕 → 再打印日志
}
2. 不要在条件语句中调用 useEffect
// ❌ 错误:不能在条件语句中调用
if (condition) {
useEffect(() => { ... });
}
// ✅ 正确:在组件顶层调用,通过依赖数组或内部逻辑控制
useEffect(() => {
if (!condition) return;
// ...
}, [condition]);
3. Effect 不应该用于转换渲染所需的数据
// ❌ 错误:应该使用 useMemo
const [data, setData] = useState([]);
const [sorted, setSorted] = useState([]);
useEffect(() => {
setSorted([...data].sort()); // 不必要的额外渲染
}, [data]);
// ✅ 正确:在渲染期间计算
const sorted = useMemo(() => [...data].sort(), [data]);
// ✅ 或者直接在渲染期间计算(简单场景)
const sorted = [...data].sort();
标签分类
# React# 前端