它是干嘛的
和外部系统同步:DOM、网络、订阅、定时器、第三方库。
useEffect(() => {
// 副作用
return () => {
// 清理(组件卸载或依赖变前跑)
};
}, [dep1, dep2]);
执行时机:commit 之后、浏览器绘制后。
依赖数组的三种写法
useEffect(fn); // 每次 render 都跑——基本没用
useEffect(fn, []); // 只跑一次(挂载时)+ 清理跑一次(卸载时)
useEffect(fn, [a, b]); // 任一变化时跑(+ 上次的清理)
ESLint 会逼你写所有依赖
useEffect(() => {
fetch(`/api/${id}`).then(...);
}, []); // ⚠️ eslint-plugin-react-hooks 会提示 "id 缺少"
遵守它——少写依赖几乎一定有 bug。
大部分时候你不需要 useEffect
官方文档整整一章在讲这个。
❌ 用 effect 派生 state:
useEffect(() => setFullName(`${first} ${last}`), [first, last]);
✅ render 里算:
const fullName = `${first} ${last}`;
❌ 用 effect 在事件之后做事:
function Form() {
const [submitted, setSubmitted] = useState(false);
useEffect(() => { if (submitted) showToast(); }, [submitted]);
const onSubmit = () => setSubmitted(true);
}
✅ 事件回调里直接做:
const onSubmit = () => { showToast(); };
✅ 用 effect 的场合:
- 订阅外部数据(WebSocket、EventEmitter)
- 集成第三方库(地图、图表、富文本)
- 浏览器 API(document.title、IntersectionObserver)
- 数据获取(但 Server Components / TanStack Query / SWR 更好)
清理函数
useEffect(() => {
const ws = new WebSocket(url);
ws.onmessage = ...;
return () => ws.close(); // 卸载前 / 依赖变前先关
}, [url]);
没写清理 = 内存泄漏 + 重复订阅。订阅类副作用必带清理。
竞态条件
useEffect(() => {
fetch(`/api/${id}`).then(r => r.json()).then(setData);
}, [id]);
id 从 1 切到 2,两个请求都飞了;1 的响应可能比 2 晚到 → UI 显示 1 的数据但 id 是 2。
修法:
useEffect(() => {
let cancelled = false;
fetch(`/api/${id}`).then(r => r.json()).then(data => {
if (!cancelled) setData(data);
});
return () => { cancelled = true; };
}, [id]);
或用 AbortController:
useEffect(() => {
const ctrl = new AbortController();
fetch(`/api/${id}`, { signal: ctrl.signal }).then(...).catch(...);
return () => ctrl.abort();
}, [id]);
或——用 TanStack Query,竞态它内部处理。
StrictMode 下 effect 跑两次
开发模式 + <StrictMode> 故意把 effect 跑两遍(挂载 → 清理 → 重新挂载),就是为了暴露你没正确清理。生产环境只跑一次。
如果你的代码挂两次会出 bug——bug 不在 React,在你。
经典反模式
// 模拟 componentDidMount
useEffect(() => { ... }, []);
心态错误。"我想在挂载时跑一次"通常意味着:
- 应该改成 Server Component 在服务端跑
- 或者数据来源应该是父组件传 prop
- 或者用专门的数据获取库
直接看 react.dev/learn/you-might-not-need-an-effect,把例子过一遍。
自检清单
写 useEffect 前问自己:
- 这件事能不能在 render 里直接算?
- 这件事能不能放事件回调?
- 我清理了吗?
- 我处理竞态了吗?
- 依赖数组完整吗?
→ 下一篇 useRef 与命令式句柄