在现代 React 开发中,函数式组件配合 Hooks 已成为主流开发范式。其中,useState 和 useEffect 是最基础、最常用的两个内置 Hook。它们分别负责管理组件的响应式状态 和处理副作用逻辑。本文将结合代码示例与深入分析,带你全面掌握这两个核心 Hook 的使用方式、底层思想以及常见陷阱。
一、useState:让函数组件拥有"记忆"
1.1 基本用法
useState 是 React 提供的第一个 Hook,用于在函数组件中声明状态变量:
javascript
import { useState } from "react";
export default function App() {
const [num, setNum] = useState(1);
return (
<div onClick={() => setNum(num + 1)}>
{num}
</div>
);
}
这里 num 是当前状态值,setNum 是更新该状态的函数。每次调用 setNum 都会触发组件重新渲染,并使用新的状态值。
⚠️ 注意:不要直接修改状态(如
num++),必须通过setNum触发更新,否则 React 无法感知变化,也就无法触发视图的更新。
1.2 初始值支持函数形式
当初始状态需要复杂计算时,可以传入一个纯函数 作为 useState 的参数:
ini
const [num, setNum] = useState(() => {
const num1 = 1 + 2;
const num2 = 2 + 3;
return num1 + num2; // 返回 6
});
这个函数只在组件首次渲染时执行一次,后续更新不会再次调用。这有助于避免不必要的性能开销。
✅ 关键点:该函数必须是同步的、无副作用的纯函数 。不能包含
setTimeout、fetch等异步操作,因为状态必须是确定的,如果是类似于fetch这种异步请求,它的状态是不确定的。
1.3 更新状态时使用函数式更新
当新状态依赖于前一个状态时,推荐使用函数式更新:
scss
<div onClick={() => setNum(prevNum => prevNum + 1)}>
{num}
</div>
prevNum会接收最新的num状态值,这种方式能确保你总是基于最新的状态值进行计算。
二、useEffect:处理副作用的"生命周期钩子"
如果说 useState 赋予组件"记忆",那么 useEffect 就赋予组件"行动能力"------执行那些不属于纯渲染逻辑的操作,比如数据请求、订阅、定时器等。
2.1 基本结构
scss
useEffect(() => {
// 副作用逻辑
return () => {
// 清理函数(可选)
};
}, [dependencies]); // 依赖数组
- 第一个参数:副作用函数
- 第二个参数:依赖项数组(决定何时重新执行)
- 返回值(可选):清理函数,在下次 effect 执行前或组件卸载时调用
2.2 三种典型使用场景
场景一:模拟 componentDidMount(挂载时执行一次)
scss
useEffect(() => {
console.log('组件已挂载');
queryData().then(data => setNum(data));
}, []); // 空依赖数组
📌 注意:空数组
[]表示"仅在挂载时执行一次"。但如果组件被卸载后重新挂载,仍会再次执行。
场景二:监听状态变化(类似 watch)
scss
useEffect(() => {
console.log('num 发生变化:', num);
}, [num]); // 依赖 num
- 首次渲染时执行一次
- 每当
num变化时重新执行
场景三:无依赖项(每次渲染后都执行)
javascript
useEffect(() => {
console.log('每次渲染后都会执行');
}); // 没有第二个参数
⚠️ 谨慎使用!容易引发无限循环或性能问题。
2.3 清理副作用:避免内存泄漏
很多副作用会创建持久资源(如定时器、事件监听器),必须在组件卸载或依赖变化时清理:
javascript
useEffect(() => {
const timer = setInterval(() => {
console.log(num); // 注意:这里打印的是 effect 创建时的 num(闭包)
}, 1000);
return () => {
console.log('清理定时器');
clearInterval(timer);
};
}, [num]);
- 每次
num变化时,先执行上一次的清理函数(clearInterval),再创建新定时器。 - 若不清理,会导致多个定时器同时运行,造成内存泄漏,每次新建的定时器那一块内存,没有办法回收了。
🔍 重要细节:
console.log(num)打印的是闭包中的旧值,不是最新状态!这是初学者常踩的坑。
三、纯函数 vs 副作用:React 的哲学基础
理解 useState 和 useEffect 的设计,离不开对 纯函数 与 副作用 的区分。
什么是纯函数?
- 相同输入 → 相同输出
- 无外部依赖(不修改外部变量)
- 无 I/O 操作(如网络请求、DOM 操作)
javascript
// 纯函数 ✅
function add(x, y) {
return x + y;
}
// 非纯函数 ❌(修改了外部数组)
function add(nums) {
nums.push(3); // 副作用!
return nums.reduce((a, b) => a + b, 0);
}
React 组件本身应尽量保持"纯":输入 props,输出 JSX。而 useEffect 正是用来隔离副作用的机制。
四、常见误区与最佳实践
❌ 误区1:在 useState 初始值中使用异步函数
dart
// 错误!useState 不支持异步
const [data, setData] = useState(async () => {
const res = await fetch('/api');
return res.json();
});
✅ 正确做法:用 useEffect 处理异步初始化:
scss
useEffect(() => {
fetch('/api').then(res => res.json()).then(setData);
}, []);
❌ 误区2:忘记清理定时器/监听器
会导致内存泄漏,尤其在路由切换或条件渲染组件时。
✅ 总是考虑是否需要返回清理函数。
❌ 误区3:依赖项遗漏或冗余
- 遗漏依赖 → 使用旧值(闭包陷阱)
- 冗余依赖 → 不必要的重复执行
五、总结
| Hook | 作用 | 关键特性 |
|---|---|---|
useState |
管理响应式状态 | 支持函数式更新、惰性初始化 |
useEffect |
处理副作用(数据请求、订阅等) | 依赖控制、自动清理、闭包陷阱 |
- 状态是组件的核心 ,
useState让函数组件具备状态管理能力。 - 副作用必须被隔离 ,
useEffect是 React 对"纯组件"理念的优雅妥协。 - 纯函数是基石,理解它才能写出可预测、可维护的 React 代码。
掌握 useState 与 useEffect,就掌握了函数式组件的"灵魂"。在实际开发中,善用它们的特性,避开常见陷阱,你的 React 应用将更加健壮、高效。
📚 延伸阅读:React 官方文档 - Hooks
希望这篇文章能帮助你更深入地理解 React Hooks 的核心思想。如果你觉得有用,欢迎点赞、收藏并在评论区交流你的实践经验!