React Hooks 入门:从状态管理到副作用处理
如果你刚开始接触 React,可能会被 useState、useEffect 这些以 use 开头的函数搞得一头雾水。别担心,它们其实就像厨房里的工具——有的负责“记住”当前做了几道菜(状态),有的负责“洗锅刷碗”(清理副作用)。今天我们就用生活化的比喻,一步步揭开 React Hooks 的面纱。
一、状态的起点:useState
React 组件默认是“无记忆”的——每次渲染都像一张白纸。但现实应用中,我们常常需要记住用户点了几次按钮、输入了什么内容。这时就轮到 useState 登场了。
useState
它是 React 内置的一个 Hook(钩子),专门用来给函数组件添加状态(state)能力,创建的是响应式状态
import { useState } from 'react';
export default function App() {
const [num, setNum] = useState(1);
return (
<div onClick={() => setNum(num + 1)}>
<h1>当前数字:{num}</h1>
</div>
);
}
这里 const [num, setNum] = useState(1) 是 JavaScript 的解构赋值语法,把 useState 返回的数组拆成两个变量:
num:当前状态值setNum:修改状态的函数
初始值也可以是个函数?
是的!如果你的初始状态需要复杂计算(比如加法、查表),可以传入一个纯函数:
const [num, setNum] = useState(() => {
const num1 = 1 + 2;
const num2 = 2 + 3;
return num1 + num2; // 返回8
});
注意:这个函数必须是同步的、确定的,不能是异步请求(比如 fetch 或 setTimeout)。为什么?因为 React 需要在组件首次渲染时立刻知道状态是什么,不能等。
React 可能在开发模式下多次调用初始化函数(例如 Strict Mode),如果函数有副作用或非确定性(比如
Math.random()),会导致状态不一致或 bug 难以复现
这就引出了一个关键概念:纯函数。
二、什么是纯函数?为什么重要?
纯函数就像一台可靠的咖啡机:你放进去相同的豆子和水量,它永远给你同样的一杯咖啡。在编程中,纯函数满足两个条件:
- 相同输入 → 相同输出
- 没有副作用(不修改外部变量、不发网络请求、不操作 DOM)
反例:
const nums = [1, 2];
function add(nums) {
nums.push(3); // 修改了外部数组!这是副作用
return nums.reduce((pre, cur) => pre + cur, 0);
}
调用 add(nums) 后,nums 本身被改变了——这会让程序变得难以预测。
而 useState 的初始化函数必须是纯函数,就是为了保证组件行为可预测、可重复,确保状态是确定的。
三、那我想加载网络数据怎么办?
既然 useState 不能处理异步,我们就需要另一个工具:useEffect。
想象你点了一份外卖(异步请求),不能在“决定吃什么”(初始化状态)的时候就拿到饭,而是要等骑手送来。useEffect 就是那个“等骑手来”的机制。
import { useState, useEffect } from 'react';
async function queryData() {
const data = await new Promise(resolve => {
setTimeout(() => resolve(666), 2000);
});
return data;
}
export default function App() {
const [num, setNum] = useState(0);
console.log(111); // 渲染日志
useEffect(() => {
console.log('xxx'); // effect 执行日志
queryData().then(data => {
setNum(data); // 2秒后设置为666
});
}, []); // 注意这个空数组!
return <div>{num}</div>;
}
这里的 [] 是依赖项数组。空数组表示:“只在组件挂载时执行一次”,相当于 Vue 中的 onMounted。
我们打印一下:
函数组件体中的代码(包括
console.log(111))在每次组件渲染时都会执行,包括首次挂载和状态更新后的重新渲染。而 useEffect 回调中的代码(console.log('xxx'))由于其依赖数组为空 [] ,只在组件挂载后执行一次,不会在后续更新中重新执行。
四、依赖项:让 effect “聪明”起来
useEffect 的依赖项决定了它何时重新运行:
[]:只运行一次(挂载时)[num]:当num变化时运行- 不写依赖项:每次渲染都运行(慎用!)
当依赖项为[num]时:
该 effect 会在组件挂载时执行,并且在
num的值发生变化(状态更新)时也会执行。
如果没有依赖项:
该 effect 会在每次组件渲染后都执行,无论组件状态或 props 是否发生变化。这通常会导致性能问题。
正确依赖项机制确保副作用在正确时机触发,避免无限循环,同时优化性能。
五、副作用的“善后”:定时器与内存泄漏
有时候副作用会留下“烂摊子”。比如你开了个定时器:
useEffect(() => {
const timer = setInterval(() => {
console.log(num);
}, 1000);
}, [num]);
return (
<>
<div onClick={() => setNum(prevNum => prevNum + 1)}>{num}</div>
</>
)
当我们多次触发点击事件让num变化,状态持续更新时:
问题来了:每次
num 变化,都会启动新的定时器,但旧的没关!结果就是多个定时器同时跑,疯狂打印,甚至导致内存泄漏——就像你开了10个水龙头却忘了关,最后家里淹了。
解决方法?返回一个清理函数:
useEffect(() => {
const timer = setInterval(() => {
console.log(num);
}, 1000);
return () => {
console.log('清除定时器');
clearInterval(timer); // 关掉水龙头!
};
}, [num]);
return (
<>
<div onClick={() => setNum(prevNum => prevNum + 1)}>{num}</div>
</>
)
我们重新执行一下程序:
这个 return 函数会在下一次 effect 执行前或组件卸载时自动调用,确保资源被回收。
再看一个例子:
// App.jsx
return (
<>
<div onClick={() => setNum(prevNum => prevNum + 1)}>{num}</div>
{num % 2 === 0 && <Demo />}
</>
)
// Demo.jsx
export default function Demo() {
useEffect(() => {
const timer = setInterval(() => {
console.log('timer');
}, 1000);
return () => clearInterval(timer); // 卸载时清理
}, []);
return <div>偶数Demo</div>;
}
{num % 2 === 0 && <Demo />}
这意味着:
- 当
num是偶数时,<Demo />被渲染(挂载) ; - 当
num变成奇数时,<Demo />被移除(卸载) 。
当父组件不再渲染 <Demo />(比如 num 变成奇数),React 会自动调用 return 里的清理函数,避免“幽灵定时器”。
六、useEffect 的三种典型执行模式小结
通过前面的例子,我们可以把 useEffect 的行为归纳为以下三种典型模式,这也是理解其工作机制的关键:
1. 挂载时执行一次(模拟 onMounted)
useEffect(() => {
// 初始化操作:请求数据、绑定事件等
}, []);
- 依赖项为 空数组
[] - 效果:仅在组件首次挂载后执行一次,后续更新不再触发
- 用途:替代 Vue 中的
onMounted,适合一次性初始化逻辑
2. 依赖变化时执行(响应式更新)
useEffect(() => {
// 当 userId 或 theme 变化时重新执行
}, [userId, theme]);
- 依赖项为 状态或 props 的数组
[a, b] - 效果:组件挂载时执行一次,之后每当任一依赖项发生变化,effect 就重新运行
- 用途:监听特定数据变化并作出响应,比如根据搜索关键词重新获取列表
3. 清理副作用(防止内存泄漏)
useEffect(() => {
const timer = setInterval(() => { /* ... */ }, 1000);
return () => {
// 清理上一次 effect 留下的副作用
clearInterval(timer);
};
}, [deps]);
-
如果 effect 返回一个函数,React 会将其作为清理函数(cleanup function)
-
调用时机:
- 下一次该 effect 重新执行前(用于清除旧资源)
- 组件卸载时(确保彻底释放)
-
用途:清除定时器、取消网络请求、解绑事件监听器等,避免“幽灵副作用”
💡 这三个特性共同构成了
useEffect的完整生命周期管理能力:初始化 → 响应更新 → 清理收尾。它不是简单的“副作用钩子”,而是一个可控制、可清理、可依赖追踪的副作用管理系统。
总结:Hooks 的协作逻辑
useState:管理确定的状态,初始值必须来自纯函数。- 纯函数 vs 副作用:状态初始化要“干净”,异步、定时器、DOM 操作都是“脏活”。
useEffect:专门处理副作用,通过依赖项控制执行时机。- 清理函数:负责“擦屁股”,防止内存泄漏,确保应用健壮。
React Hooks 就像一套精密的厨房系统:useState 是你的食材清单,useEffect 是你的灶台和洗碗池。只有每一步都规范操作,才能做出既美味又安全的“前端大餐”。