太棒了!👏 你已经掌握了组件如何“说话”,现在是时候让它们“对外联系”了!
—— 让 React 连接外部世界
⚠️ 这是 React 中最强大也最容易被误解的 Hook。
掌握它,你就能:
- 从 API 获取数据
- 设置定时器
- 监听键盘/窗口事件
- 操作 DOM
- 清理资源(防内存泄漏)
一、什么是“副作用”?🧬
在 React 中,副作用(Side Effect) 是指那些不在渲染过程中发生,但会影响应用的行为,比如:
| 常见副作用 | 示例 |
|---|---|
| 数据请求 | fetch('/api/user') |
| 订阅/监听 | window.addEventListener |
| 手动修改 DOM | ref.current.focus() |
| 设置定时器 | setInterval |
| 日志打印 | console.log(开发用) |
✅ 简单说:
渲染函数(组件)应该是“纯”的 —— 输入 props/state → 输出 JSX。
所有“会改变外部世界”的操作,都应放在 useEffect 中。
二、useEffect 基本语法 🧩
useEffect(() => {
// 副作用逻辑(如:请求数据、添加监听)
return () => {
// 清理函数(可选)—— 组件卸载时执行
// 如:清除定时器、取消订阅、断开连接
};
}, [依赖项数组]); // 控制执行时机
三、三种使用模式(必掌握)✅
🔹 模式 1:仅在组件挂载时执行一次
👉 相当于类组件的 componentDidMount
useEffect(() => {
console.log('组件已挂载');
// 常用于:首次加载数据
fetchUserData();
}, []); // 依赖数组为空 → 只执行一次
✅ 适用场景:
- 获取初始数据(用户信息、文章列表)
- 设置一次性监听器
- 发送埋点日志
🔹 模式 2:在特定数据变化时执行
👉 相当于 componentDidUpdate
useEffect(() => {
console.log('用户名变了,重新加载数据');
fetch(`/api/user/${userId}`);
}, [userId]); // 当 userId 改变时重新执行
✅ 适用场景:
- 根据路由参数加载不同内容
- 搜索关键词变化时发起请求
- 表单字段变化时验证或预览
🔹 模式 3:每次渲染后都执行(慎用)
👉 没有依赖数组
useEffect(() => {
document.title = `当前用户:${name}`;
// 每次 name 变化都会执行
});
// 没写依赖数组 → 每次渲染后都执行
⚠️ 警告:容易导致无限循环或性能问题,建议明确写出依赖项。
四、实战案例 💡
🌐 案例 1:从 API 获取用户数据
import { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(true);
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => {
setUser(data);
setLoading(false);
})
.catch(err => {
console.error('加载失败', err);
setLoading(false);
});
}, [userId]); // 用户ID变化时重新请求
if (loading) return <p>加载中...</p>;
if (!user) return <p>用户不存在</p>;
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}
⏱️ 案例 2:构建一个倒计时组件
function Countdown({ seconds }) {
const [timeLeft, setTimeLeft] = useState(seconds);
useEffect(() => {
// 设置定时器
const timer = setInterval(() => {
setTimeLeft(prev => {
if (prev <= 1) {
clearInterval(timer); // 清除定时器
return 0;
}
return prev - 1;
});
}, 1000);
// 清理函数:组件卸载时清除定时器
return () => clearInterval(timer);
}, []); // 只在挂载时设置一次
return <div>剩余时间:{timeLeft} 秒</div>;
}
✅ 关键点:
return () => clearInterval(timer)防止内存泄漏- 即使组件被销毁,定时器也会继续运行(除非清理)
🎧 案例 3:监听键盘事件(全局快捷键)
function App() {
useEffect(() => {
const handleKeyDown = (e) => {
if (e.key === 'Escape') {
alert('你按了 Esc 键!');
}
};
window.addEventListener('keydown', handleKeyDown);
// 清理:移除监听器
return () => {
window.removeEventListener('keydown', handleKeyDown);
};
}, []); // 只绑定一次
return <div>按 Esc 键试试</div>;
}
五、常见陷阱与最佳实践 ⚠️✅
❌ 错误 1:忘记依赖项 → 闭包陷阱
// ❌ 错误:count 是旧值,永远是 0
useEffect(() => {
const timer = setInterval(() => {
console.log(count); // 始终是初始值
}, 1000);
}, []); // 没依赖 count → 拿不到最新值
✅ 正确做法:
// 方案 1:加依赖 → 每次 count 变化重建定时器
useEffect(() => {
const timer = setInterval(() => {
console.log(count);
}, 1000);
return () => clearInterval(timer);
}, [count]);
// 方案 2:用函数式更新 + ref(高级)
// 或使用 useInterval 自定义 Hook
❌ 错误 2:异步函数直接作为 useEffect 回调
// ❌ 错误写法
useEffect(async () => {
const res = await fetch('/api/data');
setData(await res.json());
}, []);
✅ 正确做法:在内部定义异步函数
useEffect(() => {
const loadData = async () => {
try {
const res = await fetch('/api/data');
setData(await res.json());
} catch (err) {
console.error(err);
}
};
loadData();
}, []);
✅ 最佳实践总结
| 建议 | 说明 |
|---|---|
| 明确写出依赖项 | 避免闭包陷阱,让 React 精确控制执行时机 |
| 清理副作用 | 定时器、事件监听、订阅必须清理 |
| 拆分逻辑 | 不同目的的副作用分开写多个 useEffect |
| 使用 ESLint 插件 | eslint-plugin-react-hooks 可自动检测依赖遗漏 |
六、实战练习 🏋️♀️
✅ 练习 1:天气组件
- 组件接收
cityprop - 使用
useEffect在city改变时请求天气 API(可用 mock 数据) - 显示温度和天气描述
// mock 请求
const fetchWeather = (city) =>
Promise.resolve({ temp: 25, desc: '晴' });
✅ 练习 2:页面标题更新器
- 创建一个
PageTitle组件 - 接收
titleprop - 使用
useEffect动态设置document.title - 组件卸载时恢复为默认标题(如 "我的网站")
✅ 练习 3:窗口大小监听器
- 使用
useEffect监听window.resize - 保存当前窗口宽度到 state
- 显示 “当前宽度:xxx px”
- 卸载时移除监听器
✅ 总结:useEffect 核心要点
| 概念 | 要点 |
|---|---|
| 执行时机 | 渲染完成后(异步) |
依赖数组 [] | 控制何时重新执行 |
| 清理函数 | 防止内存泄漏,必须写 |
| 常见用途 | 数据请求、事件监听、定时器、DOM 操作 |
不能直接 async | 需在内部调用异步函数 |
| 避免无限循环 | 检查依赖项是否导致重复执行 |
🎯 下一步预告:
你已经能让组件“对外交互”了!接下来我们要更进一步:
➡️ 自定义 Hook:封装可复用逻辑
—— 把 useEffect + useState 组合起来,写出自己的 Hook,比如 useFetch, useLocalStorage
是否继续?我将带你进入 第五课:自定义 Hook 的艺术。