“状态驱动视图,副作用管理生命周期。”
—— React 开发者的日常信条 💡
今天我们将围绕 React Hooks 中最基础也最重要的两个函数 —— useState 和 useEffect,深入剖析它们的使用方式、底层思想以及常见误区。
🔹 第一部分:纯函数 vs 副作用 —— 理解 React 的“纯净”哲学
在进入 Hooks 之前,我们必须先理解一个关键概念:纯函数(Pure Function) 。
const add = function (x, y) {
return x + y;
}
这是一个典型的纯函数:
- 相同输入 → 相同输出 ✅
- 不修改外部变量 ❌
- 不发起网络请求 ❌
- 不操作 DOM ❌
而下面这段代码就不是纯函数:
function add(nums) {
nums.push(3); // 修改了外部数组!
return nums.reduce((pre, cur) => pre + cur, 0);
}
它产生了副作用(Side Effect) —— 改变了传入的 nums 数组。这在 React 函数组件中是大忌,因为组件本身应尽可能是一个纯函数:接收 props,返回 JSX。
🎯 React 组件 = 纯函数(理想状态)
但现实世界充满副作用:API 请求、定时器、订阅事件……怎么办?
答案就是:用 useEffect 来隔离副作用!
❓答疑解惑:为什么 React 强调“纯函数”?
Q:组件为什么不能有副作用?
A:因为 React 需要可预测性。如果每次渲染都偷偷改全局变量或发请求,调试将极其困难。通过把副作用集中到 useEffect,React 能精确控制它们的执行时机(挂载、更新、卸载),保证渲染过程干净、可复现。
Q:fetch 是异步的,能放进纯函数吗?
A:不能!所以 fetch 必须放在 useEffect 中。这也是为什么你在 useState 的初始化函数里不能写异步逻辑——它必须是同步纯函数。
🔹 第二部分:useState —— 让函数组件拥有“记忆”
React 函数组件默认是无状态的。但通过 useState,我们可以赋予它响应式状态:
const [num, setNum] = useState(() => {
const num1 = 1 + 2;
const num2 = 2 + 3;
return num1 + num2; // 同步计算初始值
});
这里的关键点:
useState接收一个初始值,可以是直接值(如1),也可以是一个同步函数(用于复杂计算)。- 返回一个数组:
[当前状态, 更新函数] setNum可以接受新值,也可以接受一个函数:setNum(prev => prev + 1),这能避免闭包陷阱。
💡 状态的本质:变化的数据。用户点击 → 触发
setNum→ 状态更新 → 组件重新渲染 → 视图变化。
❓答疑解惑:关于 useState 的常见疑问
Q:为什么 useState 的初始化函数必须是同步的?
A:因为 React 需要在首次渲染时立即确定初始状态。如果允许异步(比如 await fetch()),组件就无法知道该显示什么,会卡住。异步数据应在 useEffect 中获取,再用 setNum 更新。
Q:为什么推荐用 setNum(prev => prev + 1) 而不是 setNum(num + 1)?
A:这是为了避免闭包问题。在事件处理器或异步回调中,num 可能是旧值(被闭包捕获)。而 prev 总是 React 内部最新的状态值,更安全。
Q:状态更新是同步还是异步?
A:在 React 事件处理器中,setNum 是批量异步更新的(为了性能优化)。但在原生事件或 setTimeout 中,可能是同步的。不过你不应依赖其同步性,应始终当作异步处理。
🔹 第三部分:useEffect —— 副作用的“管家”
当你的组件需要:
- 发起 API 请求 🌐
- 设置定时器 ⏰
- 订阅事件 📡
- 手动操作 DOM 🖱️
这些副作用必须交给 useEffect:
useEffect(() => {
console.log('effect');
const timer = setInterval(() => {
console.log(num);
}, 1000);
return () => {
console.log('remove');
clearInterval(timer); // 清理上一次的副作用
};
}, [num]);
关键机制:
- 第一个参数:副作用函数(可包含异步逻辑)
- 第二个参数:依赖数组,决定何时重新执行
- 返回函数:清理函数,在下一次 effect 执行前或组件卸载时调用
三种依赖情况:
[]:只在挂载时执行一次(类似componentDidMount)[a, b]:当a或b变化时执行- 不传:每次渲染后都执行(慎用!)
⚠️ 注意:
useEffect中的num是闭包捕获的值。如果依赖没写全,可能读到旧状态!
❓答疑解惑:useEffect 的高频困惑
Q:为什么定时器要清理?不清理会怎样?
A:如果不清理,组件卸载后定时器仍在运行,会尝试更新已卸载组件的状态,导致内存泄漏和警告。清理函数就是“善后”。
Q:依赖数组写错了会有什么后果?
A:两种典型错误:
- 漏写依赖:effect 读取旧状态,逻辑出错。
- 多写无关依赖:effect 频繁执行,性能下降。
建议开启 ESLint 的 react-hooks/exhaustive-deps 规则自动检查。
Q:能在 useEffect 里直接写 async/await 吗?
A:不能直接写,因为 useEffect 的第一个参数必须是普通函数(不能是 async 函数,因其返回 Promise)。正确做法:
useEffect(() => {
const fetchData = async () => {
const data = await queryData();
setNum(data);
};
fetchData();
}, []);
🔚 总结:Hooks 的设计哲学
| 概念 | 作用 | 对应 Hook |
|---|---|---|
| 状态 | 存储变化的数据 | useState |
| 副作用 | 处理非纯操作(请求、定时器等) | useEffect |
React 通过这两个核心 Hook,将数据流与副作用管理清晰分离:
- 组件主体保持纯净(纯函数风格)
- 副作用被隔离、受控、可清理
这正是现代 React 应用可维护、可测试、高性能的基石 🏗️。
🌟 最后提醒
- 初始化状态 → 用
useState+ 同步纯函数 - 获取异步数据 → 用
useEffect+fetch/axios - 清理资源 → 别忘了
return () => {} - 依赖数组 → 写全、写对,让 ESLint 帮你
掌握这些,你就真正理解了 React Hooks 的“道”而非“术”🚀。
Happy coding! 💻✨