引言:React Hooks 的核心价值
在 React 16.8 版本引入 Hooks 之前,React 函数组件是 "无状态" 的,开发者无法直接管理内部状态或执行副作用操作。如果需要使用这些能力,就必须使用类组件。然而,类组件语法冗长、逻辑复用困难、生命周期方法割裂业务逻辑,导致代码难以维护。
React Hooks 的出现彻底改变了这一局面。它允许开发者在不编写类的前提下,使用状态(state)和副作用(effects)等原本仅限于类组件的能力。
在众多内置的 Hooks 中,useState 和 useEffect 是最基础、最常用的两个,堪称函数式 React 开发的两大"基石":
useState赋予函数组件管理本地状态的能力,是构建交互式 UI 的起点;useEffect统一处理各种副作用逻辑,实现了声明式副作用管理。
因此,深入理解 useState 和 useEffect,不仅是掌握 React 函数组件开发的关键,更是走向现代 React 工程开发的第一步。无论是构建简单计数器,还是复杂的数据驱动应用,这两个 Hooks 都扮演着不可或缺的角色。
第一部分:探索 useState
什么是 useState?
useState 是 React 提供的一个 Hook(钩子) ,它让函数组件拥有了"记忆"能力。
函数内的普通变量在函数执行后就会被销毁,如果我们下次再执行这个函数,中间的过程就需要重新执行一遍。
而利用 useState 机制"钩住"数据,此时数据就变成状态(state)了,即使重新运行函数,数据也状态也不会重置,除非卸载组件。
例如:
JSX
import { useState } from 'react';
export default function CounterDemo() {
// 普通变量:每次函数执行都会重置
let normalCount = 0;
// 状态变量:被 React "钩住",不会重置
const [hookCount, setHookCount] = useState(0);
// 每次点击,两个计数都尝试 +1
const handleClick = () => {
normalCount = normalCount + 1; // 修改普通变量
setHookCount(hookCount + 1); // 更新状态
console.log('普通变量:', normalCount);
console.log('状态变量:', hookCount + 1); // 注意:这里还没重新渲染
};
return (
<div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
<h2>普通变量 vs useState 状态</h2>
<p>普通变量(每次渲染重置为 0): {normalCount}</p>
<p>状态变量(持久保存): {hookCount}</p>
<button onClick={handleClick}>点击 +1</button>
</div>
);
}
效果如下:

每次执行都会重新初始化所有变量,无法保留用户交互或数据变化的结果。但是通过 useState,我们可以:
- 声明一个持久化的状态变量
- 获取一个专门用于更新该状态的函数
- 实现响应式 UI:当状态改变时,React 自动重新渲染组件,使视图与数据保持同步
核心价值 :
useState将函数组件从"一次性快照"转变为能随用户交互动态变化的活组件,状态(state)是变化的数据,也是组件的核心。
2. 基本使用方法
useState 的语法: const [num, setNum] = useState(初始值)
-
状态变量
num:当前状态值 -
更新变量的函数
setNum:更新状态的函数
jsx
import { useState } from 'react';
function MyComponent() {
// 解构赋值:[当前状态, 更新函数] = useState(初始值)
const [num, setNum] = useState(1);
return (
<div onClick={() => setNum(num + 1)}>
当前值: {num}
</div>
);
}
useState 支持两种初始化方式:
方式一:直接传入初始值 适用于简单、静态的初始值
jsx
const [count, setCount] = useState(0);
方式二:传入初始化函数 当状态初始值需要经过复杂计算,就可以配置函数来计算
jsx
const [num, setNum] = useState(() => {
const num1 = 1 + 2;
const num2 = 2 + 3;
return num1 + num2; // 返回 6
});
关键要求:
- 函数必须为同步函数,异步的函数结果不确定,而状态一定要是确定的。
- 函数必须是纯函数:指每次传入相同的输入始终返回相同的输出,且无副作用的函数(不修改外部状态,不依赖外部状态,不改变传入的状态)
3. 状态更新机制
利用setState()函数进行状态更新不单单是"修改某个变量",并且触发了 React 的响应式更新循环:
| 方式 | 代码示例 | 适用场景 |
|---|---|---|
| 直接传值 | setNum(num + 1) |
简单更新,不依赖旧状态 |
| 函数式更新 | setNum(prev => prev + 1) |
确保基于最新状态更新,避免闭包陷阱 |
React 的核心思想:数据驱动视图
React 的设计哲学可以用一句话精准概括:"视图是状态的映射,交互是改变状态的手段" ------ View = f(State)
而这就意味着:UI 界面完全由当前应用的状态(State)决定。只要状态相同,渲染出的界面就一定相同。这种"数据驱动视图"的模式,构成了 React 响应式更新机制的基础。
并且在 React 应用中,一切交互与渲染都围绕以下三个基本要素展开:
State(状态/数据)
- 是应用的"心脏",存储着当前的数据(例如
num = 1)。 - 在函数组件中,通常通过
useState、useReducer或状态管理库来管理。 - State 必须被视为不可变 ------不能直接修改,只能通过 React 提供的 set 函数请求更新。
View(视图/UI)
- 用户看到的界面,由 JSX 描述并最终转化为 DOM。
- 组件本质上是一个函数:输入是 props 和 state,输出是 UI。
- 每次 state 改变,React 会自动重新执行组件函数,生成新的 View。
Event(事件/交互)
- 用户对界面的操作,如点击按钮(
onClick)、输入文本(onChange)等。 - 事件处理函数是连接用户行为与状态更新的桥梁。
而基于这三个基本要素,形成了数据驱动的闭环流程
State → View:数据驱动显示
- 含义:状态决定界面长什么样。
- 机制:React 根据当前的 State 自动计算并渲染出对应的 View。
这是"声明式编程"的体现------你只需描述"UI 应该是什么",而不是"如何一步步操作 DOM"。
View → Event:视图产生交互
- 含义:用户在界面上进行操作。
- 机制:用户在 View 上交互(比如按钮)触发了 Event。
Event → State:事件改变数据
- 含义:交互导致状态更新。
- 机制 :事件处理函数调用
setState,向 React 提出状态变更请求。
⚠️ 注意:
setState是异步的,不会立即改变当前作用域中的 state 值,而是安排下一次重新渲染时使用新值。
最后闭环形成:自动重渲染(Re-render) :用户交互 → 触发事件 → 更新状态 → 驱动视图刷新
而这个闭环让我们无需手动操作 DOM,只需关注"状态如何变化",UI 便会自动同步。
4. 实践案例分析:点击计数器
jsx
export default function App() {
const [num, setNum] = useState(() => {
const num1 = 1 + 2;
return num1; // 初始值 = 3
});
return (
<div onClick={() => setNum((prevNum) => {
console.log(prevNum); // 打印当前状态
return prevNum + 1; // 返回新状态
})}>
{num}
</div>
);
}
关键设计亮点
- 函数式更新 :
setNum(prev => ...)确保即使多次快速点击,也能基于最新状态计算。 - 纯函数初始化:初始值通过纯函数计算,符合 React 状态确定性原则。
- 响应式核心 :完美体现 View = f(State) ------ 视图是状态的纯函数映射。
第二部分:解析useEffect
useEffect的作用
在React中,useEffect钩子用于处理副作用。而副作用指的是那些 影响外部世界或依赖于外部世界的操作,例如数据获取、订阅或者手动修改DOM等。
对于组件而言,理想情况下,输入参数应直接决定输出的JSX结构,而副作用则通过useEffect来处理(纯函数 <--对立--> 副作用)
基本使用方法
useEffect 的语法:useEffect(() => { return }, [])
useEffect的第一个参数是一个普通函数,通常使用简洁的箭头函数。
- 包含需要执行的副作用逻辑(监听事件、启动定时器等),并且这个函数会在组件渲染到屏幕之后异步执行(不会阻塞浏览器绘制)
- return函数 :清理函数,常用于清理上一次副作用(类似于Vue中的
onUnmounted生命周期钩子)
jsx
useEffect(() => {
console.log('组件已渲染');
const timer = setInterval(() => console.log('tick'), 1000);
// 返回清理函数
return () => {
clearInterval(timer);
};
});
useEffect的第二个参数是一个数组,称为依赖数组。决定了useEffect何时执行:
- 空数组 [] :当没有提供依赖项时,
useEffect仅在组件首次渲染后运行一次,并且不会随着后续的更新而重新触发(类似于Vue中的onMounted生命周期钩子) - 包含特定变量的数组[var] 或 [var1, var2...] :每当数组中的任何一个变量值改变时,都会触发
useEffect的执行 - 无数组(省略第二个参数) :如果省略了依赖数组,那么
useEffect将在每次渲染之后都运行,包括初次挂载以及任何后续更新(类似于Vue中的onUpdated钩子)。
副作用清理
清理副作用是useEffect的一个十分重要的机制,如果操作不当很容易造成内存泄漏!!!
例如一个经典的定时器泄漏:
jsx
import { useEffect, useState } from "react";
export default function App() {
const [num, setNum] = useState(0);
useEffect(() => {
// 定时器副作用
// 每次执行useEffect都在新建定时器
setInterval(() => {
console.log(num);
}, 1000);
}, [num]);
return (
<>
<div onClick={() => setNum((prevNum) => prevNum + 1)}>
{num}
</div>
</>
);
}
结果如图(我稍微更改了一下样式,看的更清楚)

在这里,每次num发生变化时,都会创建一个新的定时器,并且在下次num变化前没有及时清除旧的定时器,导致了内存泄漏。
将useEffect内的函数进行修改,增加清理函数,在组件卸载或下一次执行useEffect前就会对副作用进行清理
jsx
useEffect(() => {
const timer = setInterval(() => {
console.log(num);
}, 1000);
return () => {
clearInterval(timer);// 清理资源
}
}, [num]);
只要你在
useEffect中创建了"外部资源"或"长期运行的任务",就必须提供清理函数
结合实战:
在 React 中使用 useEffect + useState 实现异步数据获取并更新状态的标准模式
核心目的:让 useState 能响应异步数据
根据上文我们知道,useState 本身是同步的,不能直接"等待"异步操作。而 useEffect 允许我们在组件挂载后(或依赖变化时)执行副作用 ,包括调用异步函数
通过在 useEffect 中:
- 调用异步函数
- 在其
.then()回调中调用setNum(data) - React 会自动触发重新渲染,使 UI 显示最新数据
这就实现了: "异步获取数据 → 更新状态 → 刷新视图" 的完整流程。
标准开发流程:异步数据获取 + 状态更新
1、使用 useState定义状态
用于存储异步获取的数据,以及可选的加载/错误状态
jsx
const [num, setNum] = useState(0); // 存储数据
2、封装异步数据获取函数
将数据请求逻辑抽离为独立函数
jsx
async function queryData() {
// 模拟网络请求
const data = await new Promise(resolve => {
// 异步执行后调用 resolve(666)
setTimeout(() => resolve(666), 2000);
});
// 提取 Promise 的内部值 666 后,执行return返回数据
return data;
}
3、在 useEffect 中调用异步函数(挂载时执行)
使用 useEffect 在组件挂载后发起请求,并在结果返回后更新状态。
jsx
useEffect(() => {
queryData().then(data => {
// setNum(data) 触发重新渲染,使得组件函数再次执行,UI显示666
setNum(data);
});
}, []); // 空依赖数组:仅在挂载时执行一次
关键点:
- 依赖数组为
[],确保只执行一次(类似 Vue 的onMounted)- 不要直接在组件顶层写
await(组件函数必须是同步的!)
5、渲染 UI(使用状态值)
直接在 JSX 中使用状态变量:
JSX
return (
<div onClick={() => setNum(prev => prev + 1)}>
{num} {/* 自动响应状态变化 */}
</div>
);
当
setNum被调用(无论是异步还是点击),React 会自动重新渲染组件。
完整标准代码示例
JSX
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('yyy')
useEffect(() => {
// 增加打印可视化代码执行时机
console.log('xxx')
queryData().then(data => {
setNum(data);
})
}, [])
return (
<>
<div onClick={() => setNum(prevNum => prevNum + 1)}>
{num}
</div>
</>
)
}
效果如下:

执行输出两次"yyy"和一次"xxx"的原因:
- 首次页面挂载渲染,执行组件函数打印yyy,UI显示0
- 当setNum()触发状态改变时重渲染,组件函数再次执行打印yyy,UI显示666
但是由于
useEffect没有添加依赖项,所以只会在页面初次挂载时执行一次,所以打印一次xxx
执行流程时间线
| 时间 | 事件 |
|---|---|
| T=0ms | 组件挂载 → App() 执行 → num=0 → 渲染 0 |
| T=0ms+ | useEffect 执行 → 调用 queryData() |
| T=2000ms | setTimeout 触发 → resolve(666) |
| T=2000ms+ | .then 执行 → setNum(666) |
| T=2000ms++ | 组件重新渲染 → 显示 666 |