hook:useEffect
- 用来定义
副作用,可以替代以前的生命周期
场景代码示例
-
【场景 1】每次渲染都执行(不建议使用):
-
每次组件更新后,都运行额外的代码
// 场景:更新页面标题 - 每次渲染都执行 useEffect(() => { document.title = `You clicked ${count} times`; }); //这里没有依赖项 -
这样网页的标题就会随着每次渲染(无论是第一次 mount 还是后面的 update),都触发副作用,进而变更
-
⚠️ 不建议使用原因: 任何状态变化都会触发,容易导致性能问题和不必要的执行
-
如果要用以前的类去写,会很麻烦,需要 componentDidMount 和 componentDidUpdate
componentDidMount() { document.title = `You clicked ${this.state.count} times`; } componentDidUpdate() { document.title = `You clicked ${this.state.count} times`; }
-
-
【场景 2】依赖变化时执行(常用):
-
数组内的依赖项变化时,才运行额外的代码,也属于副作用
// 优化场景:更新页面标题 - 仅count值变化才执行 useEffect(() => { document.title = `You clicked ${count} times`; }, [count]); //这里有依赖项 -
只有
count改变时才执行,避免了不必要的副作用
-
-
【场景 3】仅首次渲染执行(常用):
-
第一次 mount 时,请求 API 加载数据,也属于副作用
// 数据请求 - 只在首次渲染时请求 useEffect(() => { fetchData().then((data) => setData(data)); }, []); //这里是空的依赖项 -
相当于类组件的
componentDidMount,只在组件挂载时执行一次
-
-
【场景 4】依赖变化时,先执行清理函数,再执行(常用):
-
数组内的依赖项变化时,重新订阅,也属于副作用
// friend.id 变化时 —— 先执行return后的函数,取消旧订阅,再建立新订阅 useEffect(() => { ChatAPI.subscribe(friend.id, handleStatusChange); return () => ChatAPI.unsubscribe(friend.id, handleStatusChange); }, [friend.id]); //这里有依赖项 -
执行流程: friend.id 变化 → 清理函数执行(取消旧订阅)→ effect 执行(建立新订阅)
-
-
【场景 5】仅在组件卸载时执行清理:
-
组件卸载之后,运行一些额外的代码,也属于副作用
// 场景:监听事件 - 挂载时添加监听,卸载时移除 useEffect(() => { window.addEventListener("resize", handleResize); return () => window.removeEventListener("resize", handleResize); }, []); //这里是空的依赖项 -
这样子,会在卸载时,自动执行 return 后面的清理函数
-
如果要用以前的类去写,会很麻烦,需要 componentDidMount 和 componentWillUnmount,像下面这样
componentDidMount() { window.addEventListener("resize", handleResize); } componentWillUnmount() { window.removeEventListener("resize", handleResize); }
-
依赖数组的区别
| 场景 | 依赖数组 | 执行时机 | 清理时机 | 使用场景 |
|---|---|---|---|---|
| 场景 1 | 无 | 每次渲染后 | - | ⚠️ 不建议 |
| 场景 2 | [dep] | 首次 或 依赖变化 | - | 常用(数据变化) |
| 场景 3 | [] | 仅首次渲染 | - | 常用(数据请求) |
| 场景 4 | [dep] + return | 首次 或 依赖变化 | 依赖变化前 或 组件卸载时 | 常用(订阅) |
| 场景 5 | [] + return | 仅首次渲染 | 仅组件卸载时 | 常用(事件监听) |
常见应用场景
| 场景 | 是否需要清理 | 依赖数组 | 示例 |
|---|---|---|---|
| 数据请求 | ❌ | [] | fetch、axios |
| 订阅/取消订阅 | ✅ | [id] | WebSocket、消息推送 |
| 事件监听/移除 | ✅ | [] | window.addEventListener |
| 定时器设置/清除 | ✅ | [] | setTimeout、setInterval |
| 手动 DOM 操作 | ❌ | [dep] | 修改样式、聚焦元素 |
| 日志埋点 | ❌ | [dep] | 页面访问统计 |
| 本地存储同步 | ❌ | [data] | localStorage.setItem |
注意事项
-
⚠️ 必须添加所有依赖项
// ❌ 错误:使用了 count 但没有添加到依赖数组 useEffect(() => { console.log(count); // count 始终是初始值 }, []); // 缺少 count // ✅ 正确:添加所有依赖项 useEffect(() => { console.log(count); }, [count]); -
⚠️ 清理函数的时机
useEffect(() => { console.log("effect 执行"); return () => { console.log("清理执行"); }; }, [dep]); // 执行顺序: // 1. 首次渲染:effect 执行 // 2. dep 变化:清理执行 → effect 执行 // 3. 组件卸载:清理执行 -
⚠️ 异步函数不能直接作为 effect
// ❌ 错误:useEffect 的回调不能是 async useEffect(async () => { const data = await fetchData(); setData(data); }, []); // ✅ 正确:在内部定义 async 函数 useEffect(() => { const loadData = async () => { const data = await fetchData(); setData(data); }; loadData(); }, []); // ✅ 或者使用 then useEffect(() => { fetchData().then((data) => setData(data)); }, []); -
⚠️ 避免在 effect 中直接修改状态导致无限循环
// ❌ 错误:无限循环 useEffect(() => { setCount(count + 1); // 触发渲染 → 再次执行 → 无限循环 }); // ✅ 正确:添加依赖数组或使用条件判断 useEffect(() => { if (count < 10) { setCount(count + 1); } }, [count]); -
⚠️ effect 中使用的函数也需要加入依赖
const fetchUser = () => { fetch(`/api/user/${userId}`).then(/* ... */); }; // ❌ 错误:fetchUser 使用了 userId,但没有在依赖中 useEffect(() => { fetchUser(); }, []); // 缺少 fetchUser 或 userId // ✅ 方案 1:将函数移到 effect 内部 useEffect(() => { const fetchUser = () => { fetch(`/api/user/${userId}`).then(/* ... */); }; fetchUser(); }, [userId]); // ✅ 方案 2:使用 useCallback 缓存函数 const fetchUser = useCallback(() => { fetch(`/api/user/${userId}`).then(/* ... */); }, [userId]); useEffect(() => { fetchUser(); }, [fetchUser]); -
✅ 使用 React DevTools 或 ESLint 检查依赖
# 安装 ESLint 插件 npm install eslint-plugin-react-hooks --save-dev// .eslintrc.json { "plugins": ["react-hooks"], "rules": { "react-hooks/rules-of-hooks": "error", "react-hooks/exhaustive-deps": "warn" // 检查依赖项 } } -
✅ 清理函数返回值必须是函数或 undefined
// ❌ 错误:返回值不是函数 useEffect(() => { return "cleanup"; // 错误 }, []); // ✅ 正确:返回函数 useEffect(() => { return () => { console.log("cleanup"); }; }, []); // ✅ 或者不返回 useEffect(() => { console.log("no cleanup needed"); }, []);
实用技巧
-
技巧 1:组合多个 effect,分离关注点
// ✅ 推荐:将不相关的逻辑分开 function Component() { // 数据请求 useEffect(() => { fetchData(); }, []); // 事件监听 useEffect(() => { window.addEventListener("resize", handleResize); return () => window.removeEventListener("resize", handleResize); }, []); // 标题更新 useEffect(() => { document.title = title; }, [title]); } -
技巧 2:使用自定义 Hook 封装复杂逻辑
// 封装数据请求逻辑 function useUserData(userId) { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { setLoading(true); fetch(`/api/user/${userId}`) .then((res) => res.json()) .then((data) => { setUser(data); setLoading(false); }); }, [userId]); return { user, loading }; } // 使用 function Component({ userId }) { const { user, loading } = useUserData(userId); if (loading) return <div>Loading...</div>; return <div>{user.name}</div>; } -
技巧 3:使用 AbortController 取消请求
useEffect(() => { const controller = new AbortController(); fetch("/api/data", { signal: controller.signal }) .then((res) => res.json()) .then((data) => setData(data)) .catch((err) => { if (err.name === "AbortError") { console.log("Request cancelled"); } }); // 清理时取消请求 return () => controller.abort(); }, []);
与类组件生命周期的对应关系
| 类组件生命周期 | useEffect 等价写法 | 说明 |
|---|---|---|
| componentDidMount | useEffect(() => {}, []) | 挂载后执行一次 |
| componentDidUpdate | useEffect(() => {}, [dep]) | 依赖变化时执行 |
| componentWillUnmount | useEffect(() => { return cleanup }, []) | 卸载前执行清理 |
| 组合使用 | useEffect(() => {}) | 每次渲染后执行 |
注意: useEffect 的思维方式与生命周期不同,应该从「同步副作用」的角度思考,而非「生命周期方法」