React(十):useState 全方位指南 —— 函数组件的"记忆细胞"
目录
- 第一部分:基础定义 —— useState 到底是什么?
- 第二部分:基础使用 —— 三步上手 useState
- 第三部分:进阶场景 —— 玩转复杂状态
- 第四部分:专属注意事项 —— 新手避坑指南
- 第五部分:拓展 —— 实战案例与 Hook 配合
- 第六部分:自测练习题
第一部分:基础定义 —— useState 到底是什么?
1.1 生活化类比:带更新通知的便签本
想象你在厨房做菜,你需要记住"盐放了多少克"。你随手拿了一张便签纸,在上面写下 盐 = 5g,然后贴在冰箱门上。
接下来发生的事情:
- 你放了一次盐,把便签纸上的数字划掉,写上
盐 = 8g - 但神奇的是——便签纸上的字一变,灶台上的显示屏自动刷新,实时告诉你"当前已放盐 8g"
- 如果你直接用手擦便签纸,显示屏不会更新——你必须通过便签纸配套的专用笔去改写,显示屏才会跟着变
React 里的 useState 就是这样一套机制:
| 厨房场景 | React 中的对应 |
|---|---|
| 便签纸 | state 变量(存储当前值) |
| 便签纸上的内容 | 状态值(比如 5) |
| 专用笔 | setState 更新函数(唯一合法的修改入口) |
| 灶台显示屏自动刷新 | React 自动重渲染组件 |
| 手擦便签纸不更新 | 直接修改 state 不触发渲染 |
// 就像你在厨房里准备了一张便签纸和一支专用笔
const [salt, setSalt] = useState(5); // 便签纸初始写的是 5g
// 想改?只能用专用笔写
setSalt(8); // React 检测到变化 → 灶台显示屏自动刷新
一句话总结:
useState给函数组件配了一本"带自动通知功能的便签本"。你在便签本上写东西(state),React 帮你在每次内容变化时自动刷新 UI(重新渲染)。
1.2 useState 的核心作用
用最直白的话说:useState 是 React 官方提供的 Hook,它让函数组件拥有了"记忆能力"。
没有 useState 的时候,函数组件就像一台没有硬盘的电脑——每次开机(重新渲染)都从零开始,上一次的运行结果全部丢失。有了 useState,函数组件就有了"记忆细胞",可以在多次渲染之间保持数据。
useState 解决的核心问题:
- 函数组件没有
this:普通函数执行完就销毁了,下次渲染时所有的局部变量都会重新初始化。useState 把数据"寄存"在 React 内部,让它跨越渲染周期存活。 - 数据变化需要触发 UI 更新:普通变量改了 React 不知道,但 setState 一调用,React 就会自动调度一次重渲染。
1.3 对比类组件的 this.state
在 Hooks 出现之前,函数组件是"无状态"的,想要状态只能用类组件。我们对比一下:
类组件时代(旧写法):
class Counter extends React.Component {
constructor(props) {
super(props);
// 所有状态挤在一个对象里
this.state = {
count: 0,
name: "",
};
}
increment = () => {
// 通过 setState 合并更新
this.setState({ count: this.state.count + 1 });
};
render() {
return (
<div>
<p>计数:{this.state.count}</p>
<button onClick={this.increment}>+1</button>
</div>
);
}
}
函数组件 + useState(现代写法):
function Counter() {
// 每个状态独立声明,职责清晰
const [count, setCount] = useState(0);
const [name, setName] = useState("");
const increment = () => {
setCount(count + 1);
};
return (
<div>
<p>计数:{count}</p>
<button onClick={increment}>+1</button>
</div>
);
}
核心差异对比:
| 维度 | 类组件 this.state |
函数组件 useState |
|---|---|---|
| 状态组织方式 | 所有状态塞在一个大对象里 | 每个状态独立声明,按需拆分 |
| 更新方式 | this.setState 自动浅合并 |
setState 直接替换整个 state |
| 读取最新值 | this.state.xxx |
通过 state 变量直接读取 |
| 代码量 | constructor + bind + render | 一行 Hook 搞定 |
| 逻辑复用 | HOC / render props(复杂) | 自定义 Hook(简洁) |
新手重点提醒:类组件的
setState会自动把新对象合并到旧 state 中;而 useState 的 setter 不会自动合并——你传什么,它就整个替换成什么。这是新手最容易出错的差异点。
1.4 API 签名详解
const [state, setState] = useState(initialValue);
逐部分拆解:
| 部分 | 说明 |
|---|---|
useState |
React 内置 Hook,从 react 包中导入 |
initialValue |
状态的初始值,只在组件首次渲染时使用。可以是任意类型:数字、字符串、对象、数组、函数等 |
state |
当前的状态值(只读) |
setState |
更新状态的函数。调用它并传入新值,React 会调度重渲染 |
[state, setState] |
数组解构语法,名字可以任意取,但业界约定为 [xxx, setXxx] |
参数类型签名:
// 简化版 TypeScript 签名
function useState<S>(
initialState: S | (() => S),
): [S, Dispatch<SetStateAction<S>>];
// 你也可以显式指定泛型
const [count, setCount] = useState<number>(0);
const [user, setUser] = useState<User | null>(null);
React 19 特别说明:React 18 / 19 中的
useStateAPI 签名与行为没有本质变化。React 18 引入的自动批量更新(Automatic Batching)在 React 19 中继续生效且表现一致。文档中所有示例同时适用于 React 18 和 React 19。
第二部分:基础使用 —— 三步上手 useState
2.1 完整使用步骤拆解
把 useState 的使用拆成五步,每一步都搞清楚:
第一步:从 react 中导入
import { useState } from "react";
第二步:在函数组件内部(顶层)声明状态
function MyComponent() {
const [name, setName] = useState(""); // 声明一个字符串状态
// ↑ ↑ ↑
// 变量名 更新函数名 初始值(空字符串)
// ... 组件其余逻辑
}
第三步:在 JSX 中使用 state 变量
return (
<div>
<p>当前名字:{name}</p> {/* 读取状态值,显示在 UI 中 */}
</div>
);
第四步:通过事件或其他时机调用 setState 更新
const handleChange = (e) => {
setName(e.target.value); // 用户输入 → 更新状态
};
第五步:React 检测到变化 → 自动重渲染
用户输入 "小" → setName("小") → React 标记该组件需要更新
→ React 用新状态重新执行函数组件 → name 变成 "小"
→ JSX 中的 {name} 渲染出 "小" → 用户看到变化
首次渲染执行流程:
- React 调用
MyComponent()函数- 执行到
useState(""),React 检查:这个组件是第一次渲染吗?是 → 用初始值""name被赋值为"",JSX 渲染出空的<p>当前名字:</p>- React 将 state 存入内部存储,记住这个组件有一个 state,索引为 0
2.2 初始值的两种设置方式
方式一:直接传固定值(最常见)
const [count, setCount] = useState(0); // 数字
const [name, setName] = useState("小明"); // 字符串
const [isOn, setIsOn] = useState(false); // 布尔
const [list, setList] = useState([]); // 空数组
const [user, setUser] = useState(null); // null
适合场景:初始值是一个简单常量,计算成本为零。
方式二:懒初始化函数(惰性初始化)
// ❌ 不好的写法:每次渲染都会执行 expensiveComputation()
const [data, setData] = useState(expensiveComputation());
// ✅ 好的写法:传入函数,React 只在首次渲染时调用它
const [data, setData] = useState(() => expensiveComputation());
为什么需要懒初始化?
// 假设这个计算很耗时(比如从 localStorage 读大量数据并解析)
function readUserSettings() {
console.log("正在读取用户设置...(耗时的计算)");
const raw = localStorage.getItem("userSettings");
return raw ? JSON.parse(raw) : { theme: "light", fontSize: 14 };
}
function App() {
// ❌ 每次渲染都执行 readUserSettings(),浪费性能
const [settings, setSettings] = useState(readUserSettings());
// ✅ 传入函数,React 只在首次渲染时调用一次
const [settings, setSettings] = useState(() => readUserSettings());
// 等效于:const [settings, setSettings] = useState(readUserSettings);
// 区别在于:前者传入函数引用,React 会帮你只在初始化时调用
}
温馨提示:如果你看到
useState(computeSomething())而不是useState(() => computeSomething()),则computeSomething()在每次渲染时都会执行(只是除了首次外,结果会被丢弃)。对于轻量计算无所谓,但对于读 localStorage、复杂计算等场景,务必使用函数形式。
2.3 示例一:计数器(最简入门)
目标:实现一个点击按钮数字 +1 的计数器。
import { useState } from "react";
function Counter() {
// 1. 声明状态:count 初始值为 0
const [count, setCount] = useState(0);
// 2. 定义更新逻辑:每次 +1
const handleIncrement = () => {
setCount(count + 1); // 基于当前值计算新值
};
// 3. 渲染:在 JSX 中展示 count,按钮绑定点击事件
return (
<div>
<h2>计数器示例</h2>
<p>
当前计数:<strong>{count}</strong>
</p>
<button onClick={handleIncrement}>点我 +1</button>
</div>
);
}
执行流程说明:
初始渲染:
React 调用 Counter()
→ useState(0) 返回 [0, setCount]
→ count = 0,渲染出 "当前计数:0"
用户点击按钮:
→ handleIncrement() 执行
→ setCount(0 + 1) → setCount(1)
→ React 标记 Counter 需要重渲染
第二次渲染:
React 再次调用 Counter()
→ useState(0) —— React 发现这不是首次渲染,忽略参数 0,用已存储的值 1
→ count = 1,渲染出 "当前计数:1"
用户再点按钮:
→ setCount(1 + 1) → setCount(2)
→ 渲染出 "当前计数:2" ... 以此类推
2.4 示例二:文本输入框同步(受控组件)
目标:一个输入框,输入什么就实时显示什么,同时有一个清空按钮。
import { useState } from "react";
function NameInput() {
// 声明状态:用于存储输入框的值
const [text, setText] = useState("");
// 输入框变化时,同步更新状态
const handleChange = (e) => {
// e.target.value 是输入框的当前文字
setText(e.target.value);
};
// 清空输入框
const handleClear = () => {
setText("");
};
return (
<div>
<h2>文本输入框同步</h2>
{/* input 的 value 绑定 state,onChange 更新 state */}
<input
type="text"
value={text} // ← 输入框显示的内容 = state
onChange={handleChange} // ← 用户输入 → 更新 state
placeholder="请输入你的名字"
/>
{/* 实时显示输入内容 */}
<p>
你输入的是:<strong>{text || "(暂无内容)"}</strong>
</p>
{/* 清空按钮 */}
<button onClick={handleClear}>清空</button>
</div>
);
}
"受控组件"是什么意思?
普通 HTML input:用户敲键盘 → 浏览器自己管理输入框里的文字
React 受控 input:用户敲键盘 → onChange 事件 → setState → React 重渲染 → input 用 value 重新显示
用户输入 "a"
↓
onChange 触发
↓
setText("a") ← React 知道值变了
↓
组件重渲染
↓
input value="a" ← React 控制显示
这就是"受控"——input 的值被 React state 牢牢控制,而非浏览器自己管。
2.5 示例三:开关切换(布尔状态)
目标:一个开关按钮,点击切换开/关状态,并展示对应的文字和样式。
import { useState } from "react";
function Toggle() {
// 布尔类型的状态:true 表示开,false 表示关
const [isOn, setIsOn] = useState(false);
// 切换函数:取反
const handleToggle = () => {
setIsOn(!isOn); // 依赖于当前值取反
// 更安全的写法(见进阶章节):
// setIsOn(prev => !prev);
};
return (
<div>
<h2>开关切换示例</h2>
{/* 根据 isOn 的值展示不同内容 */}
<p>
当前状态:
<strong style={{ color: isOn ? "green" : "red" }}>
{isOn ? "开" : "关"}
</strong>
</p>
{/* 按钮文字和样式也随状态变化 */}
<button
onClick={handleToggle}
style={{
padding: "10px 24px",
backgroundColor: isOn ? "#4caf50" : "#f44336",
color: "white",
border: "none",
borderRadius: "4px",
cursor: "pointer",
fontSize: "16px",
}}
>
{isOn ? "关闭" : "打开"}
</button>
</div>
);
}
布尔状态的常见应用场景:
| 场景 | 状态名示例 | 初始值 |
|---|---|---|
| 开关 / 切换 | isOn |
false |
| 模态框显隐 | isOpen |
false |
| 加载中 | isLoading |
true |
| 是否已提交 | isSubmitted |
false |
| 显示/隐藏密码 | showPassword |
false |
第三部分:进阶场景 —— 玩转复杂状态
3.1 对象类型的状态管理
状态不限于数字和字符串,对象也很常见。但更新对象时有一个铁律:必须遵循不可变更新原则。
核心原则:不可变更新(Immutable Update)
❌ 错误思路:直接修改原对象,然后传给 setState
✅ 正确思路:创建一个新对象(拷贝 + 修改),然后传给 setState
为什么必须这样? React 通过引用来判断状态是否变化。如果你直接修改原对象,引用没变,React 认为"状态没变",不会重渲染。
正反对比示例:用户信息表单
import { useState } from "react";
function UserProfile() {
const [user, setUser] = useState({
name: "小明",
age: 20,
city: "北京",
});
// ❌ 错误写法:直接修改原对象
const handleWrongUpdate = () => {
user.age = 21; // 直接修改了原对象!
setUser(user); // 引用没变,React 认为没变化,不会重渲染!
};
// ✅ 正确写法一:展开运算符创建新对象
const handleCorrectUpdate = () => {
setUser({
...user, // 拷贝所有旧属性
age: 21, // 覆盖需要修改的属性
});
};
// ✅ 正确写法二:使用更新函数(当新值依赖旧值时推荐)
const handleUpdateWithFunction = () => {
setUser((prevUser) => ({
...prevUser,
age: prevUser.age + 1,
}));
};
// ✅ 修改单个字段的通用处理函数
const handleFieldChange = (field, value) => {
setUser((prev) => ({
...prev,
[field]: value, // 动态属性名
}));
};
return (
<div>
<h2>用户信息</h2>
<p>姓名:{user.name}</p>
<p>年龄:{user.age}</p>
<p>城市:{user.city}</p>
<button onClick={handleCorrectUpdate}>年龄 +1(正确)</button>
<input
value={user.city}
onChange={(e) => handleFieldChange("city", e.target.value)}
placeholder="修改城市"
/>
</div>
);
}
新手重点提醒:
{ ...user, age: 21 }这种写法叫"浅拷贝"。如果对象内部有嵌套对象,嵌套对象的引用仍然是旧的。对于深层嵌套,需要用{ ...user, address: { ...user.address, city: "上海" } }这种逐层展开的方式,或使用useImmer等工具库简化。
3.2 数组类型的状态管理
数组更新的不可变原则与对象一样。好消息是 JavaScript 数组本身就有很多"返回新数组"的方法。
数组操作速查表
| 操作 | ❌ 错误(修改原数组) | ✅ 正确(返回新数组) |
|---|---|---|
| 添加 | arr.push(x) |
[...arr, x] |
| 删除 | arr.splice(i, 1) |
arr.filter((_, idx) => idx !== i) |
| 修改 | arr[i] = x |
arr.map((item, idx) => idx === i ? x : item) |
| 插入 | arr.splice(i, 0, x) |
[...arr.slice(0, i), x, ...arr.slice(i)] |
| 排序 | arr.sort() |
[...arr].sort() |
| 反转 | arr.reverse() |
[...arr].reverse() |
| 清空 | arr.length = 0 |
[] |
正反对比示例:学生名单管理
import { useState } from "react";
function StudentList() {
const [students, setStudents] = useState(["张三", "李四"]);
const [newName, setNewName] = useState("");
// ❌ 错误:直接 push 修改原数组
const addWrong = () => {
students.push(newName); // 直接修改了原数组!
setStudents(students); // 引用没变,不会重渲染
};
// ✅ 正确:展开运算符创建新数组
const addCorrect = () => {
if (newName.trim() === "") return;
setStudents([...students, newName]); // 全新数组
setNewName(""); // 清空输入框
};
// ✅ 删除:filter 返回新数组
const removeStudent = (index) => {
setStudents(students.filter((_, i) => i !== index));
// 或者用更新函数形式(更推荐):
// setStudents(prev => prev.filter((_, i) => i !== index));
};
// ✅ 修改:map 返回新数组
const updateStudent = (index, newValue) => {
setStudents(students.map((name, i) => (i === index ? newValue : name)));
};
return (
<div>
<h2>学生名单</h2>
{/* 添加区域 */}
<input
value={newName}
onChange={(e) => setNewName(e.target.value)}
placeholder="输入新学生姓名"
/>
<button onClick={addCorrect}>添加</button>
{/* 列表展示 */}
<ul>
{students.map((name, index) => (
<li key={index}>
{name}
<button onClick={() => removeStudent(index)}>删除</button>
<button
onClick={() => {
const newName = prompt("修改姓名", name);
if (newName) updateStudent(index, newName);
}}
>
修改
</button>
</li>
))}
</ul>
</div>
);
}
3.3 多状态的声明与管理
当一个组件需要多个状态时,有两种组织方式:
方式一:多个独立 useState(推荐)
function Form() {
// 各自独立,互不干扰
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [age, setAge] = useState(18);
// 更新互不影响
return (
<form>
<input value={name} onChange={(e) => setName(e.target.value)} />
<input value={email} onChange={(e) => setEmail(e.target.value)} />
<input
type="number"
value={age}
onChange={(e) => setAge(Number(e.target.value))}
/>
</form>
);
}
优点:每个状态独立更新,互不干扰,逻辑清晰。
缺点:字段特别多时(比如 10 个以上),useState 声明会比较多。
方式二:一个 useState 管理对象(适合关联性强的字段)
function Form() {
const [formData, setFormData] = useState({
name: "",
email: "",
age: 18,
});
// 通用更新函数
const handleChange = (field, value) => {
setFormData((prev) => ({
...prev,
[field]: value,
}));
};
return (
<form>
<input
value={formData.name}
onChange={(e) => handleChange("name", e.target.value)}
/>
<input
value={formData.email}
onChange={(e) => handleChange("email", e.target.value)}
/>
<input
type="number"
value={formData.age}
onChange={(e) => handleChange("age", Number(e.target.value))}
/>
</form>
);
}
优点:一次 setFormData 可以更新多个字段。
缺点:任何字段更新都要注意不可变拷贝,容易忘写 ...prev。
选择建议
| 场景 | 推荐方式 |
|---|---|
| 字段之间相互独立(如多个开关) | 多个独立 useState |
| 字段属于同一实体(如用户信息) | 一个对象 useState |
| 字段需要一起提交(如表单) | 一个对象 useState |
| 字段的更新频率差异大 | 多个独立 useState |
3.4 状态更新的异步批量更新特性
什么是"批量更新"?
React 为了性能,会把同一个事件处理函数中的多次 setState合并成一次更新,只触发一次渲染。
function BatchDemo() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1); // 希望 +1
setCount(count + 1); // 希望再 +1
setCount(count + 1); // 希望再 +1
// 问题:点击后 count 变成多少?
};
return (
<div>
<p>count: {count}</p>
<button onClick={handleClick}>+3</button>
</div>
);
}
答案:count 变成 1,不是 3。
为什么?因为这三个 setCount(count + 1) 在同一个事件处理中执行时,count 的值是同一个快照(都是 0)。所以每条语句等价于 setCount(0 + 1)。
解决方案:使用更新函数(Updater Function)
const handleClick = () => {
setCount((prev) => prev + 1); // prev = 0 → 1
setCount((prev) => prev + 1); // prev = 1 → 2
setCount((prev) => prev + 1); // prev = 2 → 3
// 结果:count = 3
};
当传给 setState 的是一个函数时,React 会把这个函数加入更新队列,并保证每次调用时 prev 都是最新的值。
批量更新的实际案例
function BatchExample() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
const handleClick = () => {
// 两个不同状态的 setState 也会被批量处理
setCount((c) => c + 1); // 更新 1
setFlag((f) => !f); // 更新 2
// React 18/19:这两次更新合并 → 只触发一次渲染
// React 17 及以前:在 setTimeout/promise 中不会合并
};
console.log("渲染了,count =", count, "flag =", flag);
return (
<div>
<p>count: {count}</p>
<p>flag: {String(flag)}</p>
<button onClick={handleClick}>同时更新两个状态</button>
</div>
);
}
React 18/19 自动批量更新规则
| 代码位置 | React 17 | React 18/19 |
|---|---|---|
| 事件处理函数中(onClick 等) | 批量 ✅ | 批量 ✅ |
| setTimeout / setInterval 回调中 | 不批量 ❌ | 批量 ✅ |
| Promise.then / async/await 中 | 不批量 ❌ | 批量 ✅ |
| 原生事件监听中 | 不批量 ❌ | 批量 ✅ |
一句话记住:React 18 及以后,任何情况下多个 setState 都会自动批量处理。当新值依赖旧值时,一律使用更新函数
setXxx(prev => ...)最安全。
第四部分:教学专属注意事项 —— 新手避坑指南
4.1 不能在条件/循环/嵌套函数中调用 useState
错误示例
function BadComponent({ isLoggedIn }) {
// ❌ 条件判断中调用 useState
if (isLoggedIn) {
const [user, setUser] = useState(null);
}
// ❌ 循环中调用 useState
for (let i = 0; i < 5; i++) {
const [value, setValue] = useState(i);
}
// ❌ 嵌套函数中调用 useState
const handleSomething = () => {
const [temp, setTemp] = useState(""); // 不行!
};
return <div>...</div>;
}
为什么会出问题?
React 靠 Hook 的调用顺序 来区分不同的 state。每次渲染时,React 按照固定的顺序依次读取 Hook。如果某次渲染跳过了某个 Hook(被 if 包裹),顺序就乱了:
第一次渲染(isLoggedIn = true):
Hook #0: useState(null) → user state
Hook #1: useState(0) → count state
第二次渲染(isLoggedIn = false):
Hook #0: useState(0) → 本应该是 count,但 React 以为这是 user!
// count 读到了 user 的值,数据错乱!
正确做法
function GoodComponent({ isLoggedIn }) {
// ✅ 始终在顶层声明所有 Hook
const [user, setUser] = useState(null);
const [count, setCount] = useState(0);
// 条件逻辑放在 Hook 下方
if (!isLoggedIn) {
return <div>请先登录</div>;
}
return (
<div>
欢迎,{user?.name},计数:{count}
</div>
);
}
铁律:Hook 必须在函数组件的顶层调用,不能在条件、循环、嵌套函数中使用。 这是 React Hook 的"规则第一条",ESLint 的
eslint-plugin-react-hooks能帮你自动检测。
4.2 不能直接修改 state,必须用 setState
错误示例 vs 正确示例
function BadVsGood() {
const [items, setItems] = useState(["a", "b"]);
const [user, setUser] = useState({ name: "小明", age: 18 });
// ========== 数组操作 ==========
// ❌ push 直接修改原数组
const addWrong = () => {
items.push("c");
setItems(items); // 引用没变,不触发渲染!
};
// ✅ 创建新数组
const addCorrect = () => {
setItems([...items, "c"]);
};
// ========== 对象操作 ==========
// ❌ 直接修改属性
const updateWrong = () => {
user.age = 19;
setUser(user);
};
// ✅ 创建新对象
const updateCorrect = () => {
setUser({ ...user, age: 19 });
};
return (/* ... */);
}
底层逻辑图解
state 在 React 内部存储结构(简化):
componentFiber.stateHooks = [
{ value: ['a', 'b'] }, ← items 状态
{ value: { name: '小明', age: 18 } } ← user 状态
]
当你调用 setState 时:
setItems(['a', 'b', 'c'])
→ React 比较:新值 ['a', 'b', 'c'] !== 旧值 ['a', 'b'] ✅ 变了!
→ 触发重渲染
当你直接 push 时:
items.push('c') → items 变成 ['a', 'b', 'c']
setItems(items) → React 比较:items === items(同一个引用!)❌ 没变!
→ 不触发渲染
React 用的是 Object.is() 比较新旧值,引用类型靠引用地址判断。
4.3 状态更新的异步性
问题场景:setState 后立刻读取 state,拿到的是旧值
function AsyncProblem() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
console.log(count); // 期望 1,实际 0!
// 因为 setCount 是异步调度,count 的值还没变
};
return (
<div>
<p>count: {count}</p>
<button onClick={handleClick}>+1 并打印</button>
</div>
);
}
三种解决方案
function Solutions() {
const [count, setCount] = useState(0);
const handleClick1 = () => {
// 方案一:使用更新函数,在函数内拿到最新值
setCount((prev) => {
const newValue = prev + 1;
console.log("最新值(方案一):", newValue); // ✅ 拿到正确值
return newValue;
});
};
const handleClick2 = () => {
// 方案二:用 useEffect 监听 state 变化
setCount(count + 1);
// useEffect 会在渲染完成后执行,此时能拿到新值
};
// 配合方案二
// useEffect(() => {
// console.log("count 已更新为:", count);
// }, [count]);
const handleClick3 = () => {
// 方案三:用临时变量记录新值
const newCount = count + 1;
setCount(newCount);
console.log("最新值(方案三):", newCount); // ✅ 直接用临时变量
};
return (/* ... */);
}
4.4 初始值只在首次渲染生效
这是一个常见的"为什么我的状态没更新"来源:
function InitialValueTrap({ initialName }) {
// 即使 initialName 变了,name 也不会变!
const [name, setName] = useState(initialName);
console.log("父组件传来的 initialName:", initialName);
console.log("当前 name 状态:", name);
return <p>{name}</p>;
}
// 父组件
function Parent() {
const [name, setName] = useState("小明");
return (
<div>
<button onClick={() => setName("小红")}>改名</button>
<InitialValueTrap initialName={name} />
{/* 点击改名后,InitialValueTrap 中的 name 仍然是 "小明"! */}
</div>
);
}
原因:useState(initialName) 中的 initialName 只在组件首次挂载时被使用。后续重渲染时,React 会忽略这个参数,直接从内部存储中读取已保存的值。
解决方案:
function Fixed({ initialName }) {
const [name, setName] = useState(initialName);
// 方案一:用 useEffect 同步外部值
useEffect(() => {
setName(initialName);
}, [initialName]);
// 方案二:如果只是要用 prop 的值,直接用 prop,不需要 state
// 根本不需要 useState!
return <p>{name}</p>;
}
判断原则:如果一个值完全由父组件决定(子组件自己不修改它),就不要用 useState 存,直接用 prop。useState 是用来管理组件内部自己产生和变化的数据的。
4.5 避免滥用 useState
能用派生值就不要声明状态
function AbuseExample({ firstName, lastName }) {
// ❌ 滥用:fullName 完全可以从 firstName 和 lastName 计算出来
const [fullName, setFullName] = useState("");
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
// ✅ 正确做法:直接计算派生值,不需要 state
const fullName = `${firstName} ${lastName}`;
return <p>{fullName}</p>;
}
滥用识别清单
| 场景 | 是否需要 useState | 正确做法 |
|---|---|---|
| 值可以在渲染时从其他 state/prop 计算出来 | ❌ 不需要 | 直接声明普通变量计算 |
| 值不会被 UI 使用、也不影响渲染 | ❌ 不需要 | 使用 useRef |
| 值不需要在多次渲染间保持 | ❌ 不需要 | 声明普通变量 |
| 值由父组件传入且子组件不改 | ❌ 不需要 | 直接用 prop |
| 值在组件生命周期中会独立变化并影响 UI | ✅ 需要 | 使用 useState |
function SmartVsOverkill() {
const [firstName, setFirstName] = useState("张");
const [lastName, setLastName] = useState("三");
// ✅ 派生值:直接计算
const fullName = `${firstName}${lastName}`;
// ✅ 派生值:带逻辑
const displayName = firstName && lastName ? fullName : "匿名用户";
// ✅ 派生值:数组操作
const [list, setList] = useState([1, 2, 3]);
const count = list.length;
const hasItems = list.length > 0;
return (
<div>
<p>姓名:{displayName}</p>
<p>列表项数:{count}</p>
<p>{hasItems ? "有数据" : "列表为空"}</p>
</div>
);
}
第五部分:教学拓展 —— 实战案例与 Hook 配合
5.1 案例一:Todo 列表的增删改查
一个完整的 Todo 列表教学案例,涵盖数组状态的增、删、改、查以及筛选显示。
import { useState } from "react";
function TodoApp() {
// 1. 任务列表状态:数组,每项是一个 { id, text, done } 对象
const [todos, setTodos] = useState([
{ id: 1, text: "学习 React useState", done: false },
{ id: 2, text: "完成课后练习", done: false },
]);
// 2. 输入框状态
const [inputValue, setInputValue] = useState("");
// 3. 当前筛选条件:'all' | 'done' | 'undone'
const [filter, setFilter] = useState("all");
// 获取下一个 id(简单做法:最大 id + 1)
const [nextId, setNextId] = useState(3);
// ====== 增:添加新任务 ======
const addTodo = () => {
const text = inputValue.trim();
if (text === "") return; // 空内容不添加
// 创建新任务对象,追加到数组末尾
setTodos([...todos, { id: nextId, text, done: false }]);
setNextId(nextId + 1); // id 递增
setInputValue(""); // 清空输入框
};
// ====== 删:根据 id 删除任务 ======
const deleteTodo = (id) => {
setTodos(todos.filter((todo) => todo.id !== id));
};
// ====== 改:切换完成状态 ======
const toggleTodo = (id) => {
setTodos(
todos.map((todo) =>
todo.id === id ? { ...todo, done: !todo.done } : todo,
),
);
};
// ====== 改:编辑任务文本 ======
const updateText = (id, newText) => {
setTodos(
todos.map((todo) => (todo.id === id ? { ...todo, text: newText } : todo)),
);
};
// ====== 查:根据筛选条件过滤展示 ======
const filteredTodos = todos.filter((todo) => {
if (filter === "done") return todo.done;
if (filter === "undone") return !todo.done;
return true; // 'all'
});
// ====== 统计数据(派生值,不需要 useState)======
const totalCount = todos.length;
const doneCount = todos.filter((t) => t.done).length;
// 处理键盘事件:按 Enter 添加
const handleKeyDown = (e) => {
if (e.key === "Enter") addTodo();
};
return (
<div style={{ maxWidth: 500, margin: "0 auto" }}>
<h2>Todo 列表</h2>
{/* ====== 输入区域 ====== */}
<div style={{ display: "flex", gap: 8, marginBottom: 16 }}>
<input
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="输入新任务,按 Enter 添加"
style={{ flex: 1, padding: 8 }}
/>
<button onClick={addTodo}>添加</button>
</div>
{/* ====== 筛选按钮 ====== */}
<div style={{ marginBottom: 16 }}>
{[
{ key: "all", label: "全部" },
{ key: "undone", label: "未完成" },
{ key: "done", label: "已完成" },
].map(({ key, label }) => (
<button
key={key}
onClick={() => setFilter(key)}
style={{
marginRight: 8,
fontWeight: filter === key ? "bold" : "normal",
color: filter === key ? "#1976d2" : "#666",
}}
>
{label}
</button>
))}
</div>
{/* ====== 任务列表 ====== */}
{filteredTodos.length === 0 ? (
<p style={{ color: "#999" }}>暂无任务</p>
) : (
<ul style={{ listStyle: "none", padding: 0 }}>
{filteredTodos.map((todo) => (
<li
key={todo.id}
style={{
display: "flex",
alignItems: "center",
padding: "8px 0",
borderBottom: "1px solid #eee",
}}
>
{/* 复选框:切换完成状态 */}
<input
type="checkbox"
checked={todo.done}
onChange={() => toggleTodo(todo.id)}
style={{ marginRight: 8 }}
/>
{/* 任务文本 */}
<span
style={{
flex: 1,
textDecoration: todo.done ? "line-through" : "none",
color: todo.done ? "#999" : "#333",
}}
>
{todo.text}
</span>
{/* 编辑按钮 */}
<button
onClick={() => {
const newText = prompt("修改任务", todo.text);
if (newText && newText.trim()) {
updateText(todo.id, newText.trim());
}
}}
style={{ marginRight: 8 }}
>
编辑
</button>
{/* 删除按钮 */}
<button onClick={() => deleteTodo(todo.id)}>删除</button>
</li>
))}
</ul>
)}
{/* ====== 统计信息 ====== */}
<div style={{ marginTop: 16, color: "#666", fontSize: 14 }}>
共 {totalCount} 项,已完成 {doneCount} 项
</div>
</div>
);
}
教学要点回顾:这个案例综合运用了数组不可变更新(展开运算符、filter、map)、多状态管理、派生值计算、以及条件渲染,是一次完整的 useState 能力检验。
5.2 案例二:表单多字段管理
实现一个注册表单,包含用户名、密码、手机号,附带实时校验提示。
import { useState } from "react";
function RegisterForm() {
// 表单数据用对象管理(字段属于同一实体)
const [form, setForm] = useState({
username: "",
password: "",
phone: "",
});
// 错误信息也用一个对象管理
const [errors, setErrors] = useState({});
// 通用字段更新函数
const handleChange = (field, value) => {
setForm((prev) => ({ ...prev, [field]: value }));
// 更新时实时清除该字段的错误提示
if (errors[field]) {
setErrors((prev) => {
const newErrors = { ...prev };
delete newErrors[field];
return newErrors;
});
}
};
// 表单校验
const validate = () => {
const newErrors = {};
if (form.username.trim().length < 3) {
newErrors.username = "用户名至少 3 个字符";
}
if (form.password.length < 6) {
newErrors.password = "密码至少 6 位";
}
if (!/^1[3-9]\d{9}$/.test(form.phone)) {
newErrors.phone = "请输入有效的手机号";
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0; // 无错误则通过
};
// 提交
const handleSubmit = (e) => {
e.preventDefault(); // 阻止默认表单提交行为
if (validate()) {
alert("注册成功!\n" + JSON.stringify(form, null, 2));
}
};
// 重置
const handleReset = () => {
setForm({ username: "", password: "", phone: "" });
setErrors({});
};
return (
<div style={{ maxWidth: 400, margin: "0 auto" }}>
<h2>注册表单</h2>
<form onSubmit={handleSubmit}>
{/* 用户名 */}
<div style={{ marginBottom: 16 }}>
<label>用户名:</label>
<input
value={form.username}
onChange={(e) => handleChange("username", e.target.value)}
placeholder="至少 3 个字符"
style={{
display: "block",
width: "100%",
padding: 8,
marginTop: 4,
border: errors.username ? "1px solid red" : "1px solid #ccc",
}}
/>
{errors.username && (
<span style={{ color: "red", fontSize: 12 }}>
{errors.username}
</span>
)}
</div>
{/* 密码 */}
<div style={{ marginBottom: 16 }}>
<label>密码:</label>
<input
type="password"
value={form.password}
onChange={(e) => handleChange("password", e.target.value)}
placeholder="至少 6 位"
style={{
display: "block",
width: "100%",
padding: 8,
marginTop: 4,
border: errors.password ? "1px solid red" : "1px solid #ccc",
}}
/>
{errors.password && (
<span style={{ color: "red", fontSize: 12 }}>
{errors.password}
</span>
)}
</div>
{/* 手机号 */}
<div style={{ marginBottom: 16 }}>
<label>手机号:</label>
<input
value={form.phone}
onChange={(e) => handleChange("phone", e.target.value)}
placeholder="11 位手机号"
style={{
display: "block",
width: "100%",
padding: 8,
marginTop: 4,
border: errors.phone ? "1px solid red" : "1px solid #ccc",
}}
/>
{errors.phone && (
<span style={{ color: "red", fontSize: 12 }}>{errors.phone}</span>
)}
</div>
{/* 按钮 */}
<div style={{ display: "flex", gap: 12 }}>
<button type="submit">注册</button>
<button type="button" onClick={handleReset}>
重置
</button>
</div>
</form>
</div>
);
}
5.3 与 useEffect 的基础配合
useState 负责"记住数据",useEffect 负责"在数据变化时执行副作用"。下面是两个基础配合场景。
场景一:状态变化时同步到 localStorage
import { useState, useEffect } from "react";
function ThemeSwitcher() {
// 从 localStorage 读取初始主题(懒初始化)
const [theme, setTheme] = useState(() => {
return localStorage.getItem("theme") || "light";
});
// 当 theme 变化时,自动同步到 localStorage
useEffect(() => {
localStorage.setItem("theme", theme);
console.log(`主题已切换为:${theme}`);
}, [theme]); // 依赖 theme,theme 变就执行
const toggleTheme = () => {
setTheme((prev) => (prev === "light" ? "dark" : "light"));
};
return (
<div
style={{
backgroundColor: theme === "light" ? "#fff" : "#333",
color: theme === "light" ? "#333" : "#fff",
padding: 20,
borderRadius: 8,
transition: "all 0.3s",
}}
>
<p>当前主题:{theme}</p>
<button onClick={toggleTheme}>切换主题</button>
</div>
);
}
场景二:搜索防抖(state + effect 经典组合)
import { useState, useEffect } from "react";
function SearchBox() {
const [keyword, setKeyword] = useState("");
const [debouncedKeyword, setDebouncedKeyword] = useState("");
// keyword 变化后等待 500ms 再更新 debouncedKeyword
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedKeyword(keyword);
}, 500);
// 清理函数:下次 effect 执行前清除上一次的定时器
return () => clearTimeout(timer);
}, [keyword]);
// debouncedKeyword 变化时发起搜索
useEffect(() => {
if (debouncedKeyword) {
console.log("发起搜索:", debouncedKeyword);
// 实际项目中这里调用 API
}
}, [debouncedKeyword]);
return (
<div>
<input
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
placeholder="输入关键词搜索"
/>
<p>实际搜索词:{debouncedKeyword}</p>
</div>
);
}
数据流图解:
用户输入 "react"
→ keyword: "" → "r" → "re" → "rea" → "reac" → "react"
→ 每次变化都触发 useEffect(设定 500ms 计时器)
→ 但只有最后一次的计时器不会被清除
→ 500ms 后 debouncedKeyword 变为 "react"
→ 触发搜索 effect → 发起 API 请求
这样避免用户每敲一个字母就发一次请求,只在输入停顿后发一次。
第六部分:自测练习题
练习题一:基础填空
补全以下代码,实现一个点击按钮数字翻倍的计数器。
import { useState } from "react";
function DoubleCounter() {
// TODO: 声明一个 count 状态,初始值为 1
const handleDouble = () => {
// TODO: 使 count 翻倍
};
return (
<div>
<p>当前值:{/* TODO: 显示 count */}</p>
<button onClick={handleDouble}>翻倍</button>
</div>
);
}
点击查看答案
import { useState } from "react";
function DoubleCounter() {
const [count, setCount] = useState(1);
const handleDouble = () => {
setCount(count * 2);
// 更安全的写法(依赖旧值时推荐):
// setCount(prev => prev * 2);
};
return (
<div>
<p>当前值:{count}</p>
<button onClick={handleDouble}>翻倍</button>
</div>
);
}
练习题二:判断对错
以下代码是否能正常让 count 的值 +2?如果不能,请指出问题并修正。
function BadCounter() {
const [count, setCount] = useState(0);
const addTwo = () => {
setCount(count + 1);
setCount(count + 1);
};
return (
<div>
<p>{count}</p>
<button onClick={addTwo}>+2</button>
</div>
);
}
点击查看答案
不能。点击后 count 只会 +1,不会 +2。
原因:两次 setCount(count + 1) 中的 count 都是同一个快照值(0),等价于两次 setCount(1)。由于批量更新,最终结果就是 1。
修正方案:使用更新函数形式。
const addTwo = () => {
setCount((prev) => prev + 1);
setCount((prev) => prev + 1);
};
练习题三:数组操作
编写代码实现:点击按钮向列表中添加一个随机数(1-100),并显示列表中的所有数字,用逗号分隔。
点击查看答案
import { useState } from "react";
function RandomList() {
const [numbers, setNumbers] = useState([]);
const addRandom = () => {
const random = Math.floor(Math.random() * 100) + 1;
setNumbers([...numbers, random]);
// 或:setNumbers(prev => [...prev, random]);
};
return (
<div>
<button onClick={addRandom}>添加随机数</button>
<p>列表:{numbers.join(", ") || "暂无数据"}</p>
</div>
);
}
练习题四:对象更新
以下代码中,点击按钮后页面显示的年龄会变化吗?如果不会,为什么?请修正。
function UserCard() {
const [user, setUser] = useState({ name: "小明", age: 18 });
const growUp = () => {
user.age = user.age + 1;
setUser(user);
};
return (
<div>
<p>
{user.name},{user.age} 岁
</p>
<button onClick={growUp}>长大一岁</button>
</div>
);
}
点击查看答案
不会变化。
原因:user.age = user.age + 1 直接修改了原对象,然后 setUser(user) 传入的仍是同一个引用。React 用 Object.is() 比较,发现引用没变,认为状态没有变化,不会重渲染。
修正方案:
const growUp = () => {
setUser({ ...user, age: user.age + 1 });
// 或使用更新函数:
// setUser(prev => ({ ...prev, age: prev.age + 1 }));
};
练习题五:综合实战
实现一个"购物车"组件,要求:
- 初始时有三个商品,每个商品包含
id,name,price,quantity - 每个商品旁边有 + 和 - 按钮可以增减数量(数量不能小于 0,减到 0 时自动删除该商品)
- 底部显示总数量和总价(派生值,不用 useState)
点击查看答案
import { useState } from "react";
function ShoppingCart() {
const [items, setItems] = useState([
{ id: 1, name: "React 入门教程", price: 59, quantity: 1 },
{ id: 2, name: "TypeScript 实战", price: 79, quantity: 2 },
{ id: 3, name: "VSCode 键帽", price: 29, quantity: 1 },
]);
// 增加数量
const increase = (id) => {
setItems((prev) =>
prev.map((item) =>
item.id === id ? { ...item, quantity: item.quantity + 1 } : item,
),
);
};
// 减少数量
const decrease = (id) => {
setItems(
(prev) =>
prev
.map((item) =>
item.id === id ? { ...item, quantity: item.quantity - 1 } : item,
)
.filter((item) => item.quantity > 0), // 数量为 0 则移除
);
};
// 派生值:不需要 useState
const totalQuantity = items.reduce((sum, item) => sum + item.quantity, 0);
const totalPrice = items.reduce(
(sum, item) => sum + item.price * item.quantity,
0,
);
return (
<div>
<h2>购物车</h2>
{items.length === 0 ? (
<p>购物车是空的</p>
) : (
<ul>
{items.map((item) => (
<li key={item.id} style={{ marginBottom: 8 }}>
<span>{item.name}</span>
<span style={{ margin: "0 12px", color: "#666" }}>
¥{item.price} × {item.quantity}
</span>
<button onClick={() => decrease(item.id)}>-</button>
<span style={{ margin: "0 8px" }}>{item.quantity}</span>
<button onClick={() => increase(item.id)}>+</button>
</li>
))}
</ul>
)}
<hr />
<p>
总计:{totalQuantity} 件商品,¥{totalPrice}
</p>
</div>
);
}
全文总结:useState 是 React 函数组件的基石——它让函数组件拥有了"记忆"。掌握好 useState,你就能在 React 中自由地管理一切本地状态。记住三大铁律:顶层调用、不可变更新、依赖旧值时用更新函数。把这三个原则刻在脑子里,你就避开了 90% 的新手坑。