作为前端开发者,React 的 Hooks 特性自推出以来,彻底改变了函数组件的开发模式,让我们能在无类组件的情况下轻松管理状态和处理副作用。本文将从最基础的useState开始,一步步深入到useEffect的使用逻辑,结合实际代码演进过程,带你理解 React Hooks 的核心思想与实践技巧。
一、起步:用 useState 给组件赋予响应式状态
useState 的核心特性总结
useState是 React 提供的状态钩子,用于在函数组件中管理状态。- 状态是 "变化的数据",是组件的核心,状态更新会触发组件重新渲染。
- 初始值支持直接传值或函数式计算,函数式初始值适用于复杂计算场景。
- 更新状态时,函数式更新能保证获取到最新的前序状态,是推荐的写法。
基本用法:初始化状态与更新状态
jsx
import { useState } from 'react'
export default function App() {
// 方式1:直接传入初始值
// const [num, setNum] = useState(1);
// 方式2:函数式初始化(适合复杂计算的初始值)
const [num, setNum] = useState(() => {
// 初始值需要复杂计算时,用函数计算更高效(仅执行一次)
const num1 = 1 + 2;
const num2 = 2 + 3;
return num1 + num2; // 初始值为 8
});
return (
<div onClick={() => setNum((prevNum) => {
console.log(prevNum);
return prevNum + 1;
})}>
<h1>当前数值:{num}</h1>
</div>
)
}
- 传入一个无参函数 作为
useState的参数,React 会在组件首次渲染时执行一次这个函数,并将函数的返回值作为状态的初始值。
- 核心优势 : 若初始值需要复杂计算 (比如遍历数组、调用 API、大量数学运算),函数式初始化只会在组件挂载时执行一次 ;而如果直接写计算表达式(比如
useState(1+2+2+3)),虽然结果一样,但该表达式会在每次组件重渲染时都执行一次(即使结果没用),造成性能浪费。
用表格总结核心区别
| 对比项 | 直接写计算表达式(useState(fn())) |
函数式初始化(useState(fn)) |
|---|---|---|
| 执行时机 | 组件每次渲染时都执行 | 仅组件首次渲染时执行一次 |
| 返回值 | 函数的执行结果(比如数字、对象) | 函数本身(React 自动执行) |
| 性能 | 复杂计算时,重渲染会造成性能浪费 | 性能最优,无无效计算 |
| 适用场景 | 简单计算(比如useState(1+2)) |
复杂计算(比如本地存储读取、大数据处理) |

setNum接收一个函数 时,React 会把"当前最新的状态值"作为参数传进来(prevNum)。- 这种写法能避免闭包旧值的问题,确保总是基于"最新"状态做更新。
- 每次点击,控制台先打印旧值,再把
num加 1,页面自动重新渲染。(如上图所示)
纯函数要求 :React 推荐这个初始化函数是纯函数 ------ 是指相同输入始终返回相同的输出,且无副作用的函数。就像代码中计算 num1 + num2 的逻辑,没有修改外部变量,也没有依赖外部可变状态,每次执行都会得到 8,完全符合纯函数的特征。
另外要注意,这个初始化函数必须是同步函数,不支持异步操作。因为异步操作的结果是不确定的,而 React 要求状态的初始值必须是确定的,否则会导致组件初始化时的状态混乱。
这就是 React 的响应式状态核心:状态变化 → 组件重新渲染 → UI 更新。
二、进阶:用 useEffect 处理副作用
有了状态之后,我们需要处理组件的 "副作用"(比如数据请求、定时器、事件监听),这时候 useEffect 就成了核心工具。useEffect 可以看作是函数组件中生命周期的替代方案,但其灵活性远不止于此。
1. 先明确:什么是副作用?
在 React 中,副作用是指那些不属于组件渲染逻辑的操作,比如:
- 数据请求(接口调用)、定时器 / 延时器
- 操作 DOM、修改全局变量
- 注册 / 取消事件监听
这些操作可能会影响外部环境,或依赖外部环境,而 useEffect 就是用来管理这些操作的。
2. useEffect 的核心特性:依赖项数组
useEffect 的第二个参数是依赖项数组 ,它决定了 useEffect 的执行时机,这也是 useEffect 最关键的特性。我们可以根据依赖项的不同,将 useEffect 分为三种常见场景,这也是我后续拆分代码的核心依据。
三、拆分学习:useEffect 的三种核心场景
为了更清晰地理解 useEffect 的用法,我将最初的代码拆分成了三个独立的组件,分别对应 useEffect 的三种典型使用场景。
1. 场景一:仅在组件挂载时执行(空依赖项)
这是最接近类组件 componentDidMount 的用法,依赖项数组为空 [] 时,useEffect 仅在组件挂载后执行一次,清理函数仅在组件卸载时执行。
jsx
import { useEffect, useState } from 'react';
// 模拟异步请求
async function queryData() {
const data = await new Promise(resolve => {
setTimeout(() => resolve(666), 2000);
});
return data;
}
export default function MountEffect() {
const [num, setNum] = useState(0);
// 空依赖项:仅挂载时执行一次
useEffect(() => {
console.log('组件挂载:Mounted');
// 挂载后发起异步请求
queryData().then(data => {
setNum(data);
console.log('异步请求完成,num更新为:', data);
});
// 清理函数:仅卸载时执行
return () => {
console.log('组件卸载:Unmounted');
};
}, []);
return (
<div>
<h3>挂载阶段的 useEffect 演示</h3>
<div>当前num值:{num}</div>
</div>
);
}

关键特点:
- 仅在组件挂载后执行一次,适合做一次性初始化操作(比如首次请求数据、注册全局事件)。
- 清理函数仅在组件卸载时执行,用于销毁挂载时创建的副作用(比如清除定时器、取消事件监听)。
- 注意闭包陷阱 :内部代码仅捕获首次渲染的状态,后续状态更新无法感知(如需获取最新状态,可使用
useRef)。
2. 场景二:依赖项变化时执行(带状态依赖)
当依赖项数组中包含状态(如 [num])时,useEffect 会在挂载时执行一次 ,之后每次依赖项变化时重新执行。
jsx
import { useEffect, useState } from 'react';
export default function DepsEffect() {
const [num, setNum] = useState(0);
// 依赖项为 num:num变化时重新执行
useEffect(() => {
console.log('依赖项 num 变化:', '当前num:', num);
}, [num]);
console.log('组件渲染时的同步输出');
return (
<div>
<h3>带依赖项的 useEffect 演示</h3>
<div onClick={() => setNum(prev => prev + 1)}>
点击修改num:{num}
</div>
</div>
);
}

关键特点:
- 执行时机:组件渲染完成后异步执行,因此同步代码永远比
useEffect先输出。 - 响应式:随依赖项的变化而更新,适合根据状态变化执行逻辑(比如状态变化时重新请求数据)。
- 这是 React 实现 "响应式副作用" 的核心方式,类似 Vue 中的
watch。
3. 场景三:清理函数(return 函数)
useEffect 中返回的函数是清理函数 ,用于销毁上一次的副作用,避免内存泄漏。它的执行时机是:下一次 useEffect 执行前 或组件卸载时。
jsx
import { useEffect, useState } from 'react';
export default function CleanupEffect() {
const [num, setNum] = useState(0);
useEffect(() => {
console.log('创建定时器:当前num:', num);
// 创建定时器
const timer = setInterval(() => {
console.log('定时器输出 num:', num);
}, 1000);
// 清理函数:销毁上一次的定时器
return () => {
console.log('清除定时器:上一次的num:', num);
clearInterval(timer);
};
}, [num]);
return (
<div>
<h3>useEffect 清理函数演示</h3>
<div onClick={() => setNum(prev => prev + 1)}>
点击修改num:{num}
</div>
</div>
);
}

关键特点:
- 清理函数的执行时机:依赖项变化时,先执行清理函数销毁旧副作用,再执行新的
useEffect回调。 - 避免内存泄漏:这是处理定时器、事件监听的标准方式,保证副作用随组件 / 状态的生命周期同步。
- 闭包捕获:清理函数会捕获当前渲染的状态,因此能准确获取 "上一次的状态值"。
如果没有这个清理函数(return):

⚠这会导致内存泄漏!!!
四、React Hooks 学习总结
从最初的简单状态组件,到拆分后的三个核心场景,我们可以总结出 React Hooks 的几个核心思想:
1. 函数组件的核心:状态与副作用分离
useState负责管理响应式状态,让组件能根据状态变化重新渲染。useEffect负责管理副作用,让渲染逻辑和副作用逻辑分离,代码更清晰。
2. useEffect 的核心:依赖项驱动
useEffect 的行为完全由依赖项数组决定,记住这几个规则:
- 无依赖项:每次组件渲染后都执行。
- 空依赖项:仅挂载时执行一次。
- 有依赖项:挂载时执行一次,依赖项变化时重新执行。
3. 最佳实践
- 尽量使用函数式更新 :当新状态依赖旧状态时,用
setState(prev => newVal)避免状态覆盖。 - 明确依赖项:不要省略必要的依赖项,否则会导致副作用与状态不同步(可借助 ESLint 插件检查)。
- 及时清理副作用:定时器、事件监听等一定要在清理函数中销毁,避免内存泄漏。
- 函数式初始化:初始值复杂时,用函数式初始化提高性能。