React useState 完全指南:从基础用法到实际开发
React + TypeScript 学习笔记 · Hooks · 状态管理
一、useState 是什么?
useState 是 React 提供的一个 Hook,用于在函数组件中保存状态,并在状态更新后触发页面重新渲染。
简单理解:
- 普通变量:数据变化,不会主动通知 React 更新页面。
useState:数据变化后,React 会根据新状态更新页面。
1.1 最简单的例子
javascript
import { useState } from "react";
function App() {
const [count, setCount] = useState(0);
return (
<div>
<h1>{count}</h1>
<button onClick={() => setCount(count + 1)}>
+1
</button>
</div>
);
}
核心语法:
scss
const [count, setCount] = useState(0);
| 部分 | 含义 |
|---|---|
useState(0) |
创建状态,初始值为 0 |
count |
当前状态值 |
setCount |
更新状态的函数 |
执行过程:
scss
点击按钮
↓
调用 setCount(count + 1)
↓
React 保存新状态
↓
重新渲染组件
↓
页面显示新的 count
1.2 为什么使用 const?
你可能会疑惑:
scss
const [count, setCount] = useState(0);
既然 count 是 const,为什么值还能改变?
因为 React 并不是修改原来的 count,而是在每次重新渲染时,重新执行组件函数并获得新的状态值。
ini
// 第一次渲染
const count = 0;
// 调用 setCount(1) 后
// 第二次渲染
const count = 1;
这只是原理示意,并不是真正执行的代码。
二、useState 的 6 种常见用法
2.1 数字状态:计数器
这是最基础的用法。
javascript
import { useState } from "react";
function Counter() {
const [count, setCount] = useState<number>(0);
return (
<div>
<h2>当前计数:{count}</h2>
<button onClick={() => setCount(count + 1)}>
+1
</button>
<button onClick={() => setCount(count - 1)}>
-1
</button>
<button onClick={() => setCount(0)}>
重置
</button>
</div>
);
}
实际效果
当前计数
0
减 1 加 1重置
这里的 <number> 是 TypeScript 泛型,表示状态只能是 number 类型。
scss
const [count, setCount] = useState<number>(0);
setCount(10); // ✅
setCount("hello"); // ❌ 类型错误
其实这里也可以省略 <number>,TypeScript 会根据初始值 0 自动推断。
2.2 字符串状态:输入框
常用于表单、搜索框、用户名输入等。
javascript
import { useState } from "react";
function UserInput() {
const [name, setName] = useState("");
return (
<div>
<input
value={name}
onChange={(e) => setName(e.target.value)}
/>
<p>你好,{name}</p>
</div>
);
}
实际效果
请输入姓名
你好,请输入姓名
重点理解:
scss
onChange={(e) => setName(e.target.value)}
执行逻辑:
- 用户在输入框输入内容。
- 触发
onChange事件。 - 通过
e.target.value获取输入内容。 - 调用
setName()更新状态。 - React 重新渲染,页面展示新内容。
这里的输入框属于 React 的受控组件(Controlled Component),因为输入值由 React 状态控制。
2.3 布尔状态:显示与隐藏
常用于弹窗、加载状态、开关等。
javascript
function Toggle() {
const [visible, setVisible] = useState(false);
return (
<div>
<button onClick={() => setVisible(!visible)}>
切换显示
</button>
{visible && <p>这是一段隐藏内容</p>}
</div>
);
}
实际效果
显示详情
显示
核心代码:
scss
setVisible(!visible);
表示把状态取反:
false→truetrue→false
2.4 对象状态:修改用户信息
实际业务中,很多状态都是对象。
例如:
typescript
interface User {
id: number;
name: string;
age: number;
}
function UserProfile() {
const [user, setUser] = useState<User>({
id: 1001,
name: "张三",
age: 25
});
const changeName = () => {
setUser({
...user,
name: "李四"
});
};
return (
<div>
<p>姓名:{user.name}</p>
<p>年龄:{user.age}</p>
<button onClick={changeName}>
修改姓名
</button>
</div>
);
}
这里最关键的是:
css
setUser({
...user,
name: "李四"
});
...user 是 JavaScript 的展开语法,用于复制对象中的属性。
相当于:
php
setUser({
id: user.id,
name: "李四",
age: user.age
});
为什么不能直接修改?
错误写法:
ini
user.name = "李四"; // ❌ 直接修改状态对象
React 状态应该按不可变数据来处理。直接修改原对象不会主动触发重新渲染,还可能导致状态不一致。
正确写法是创建新对象:
css
setUser({
...user,
name: "李四"
});
如果新值依赖旧值,也可以这样写:
ini
setUser(prev => ({
...prev,
age: prev.age + 1
}));
2.5 数组状态:任务列表(重点)
实际开发里,经常需要保存一个列表。
例如任务管理系统:
typescript
interface Task {
id: number;
name: string;
status: string;
}
function TaskList() {
const [tasks, setTasks] = useState<Task[]>([
{ id: 1, name: "视频回查", status: "running" },
{ id: 2, name: "文章回扫", status: "pending" }
]);
const addTask = () => {
setTasks(prev => [
...prev,
{
id: 3,
name: "评论治理",
status: "pending"
}
]);
};
return (
<div>
{tasks.map(task => (
<p key={task.id}>
{task.name} - {task.status}
</p>
))}
<button onClick={addTask}>
添加任务
</button>
</div>
);
}
核心:
ini
setTasks(prev => [...prev, newTask]);
表示:
- 获取旧数组
prev。 - 使用
...prev复制旧数组内容。 - 添加
newTask。 - 生成一个新数组。
- 触发 React 更新。
实际效果
任务列表
2 条任务
视频回查
running
文章回扫
pending
添加任务重置列表
数组状态常见的几种操作:
| 操作 | 推荐写法 |
|---|---|
| 添加 | setList(p => [...p, item]) |
| 删除 | setList(p => p.filter(x => x.id !== id)) |
| 修改 | setList(p => p.map(x => ...)) |
| 清空 | setList([]) |
例如修改任务状态:
ini
setTasks(prev =>
prev.map(task =>
task.id === 1
? { ...task, status: "success" }
: task
)
);
不推荐:
scss
tasks.push(newTask); // ❌ 直接修改原数组
setTasks(tasks); // ❌ 仍然是原来的数组引用
因为 React 可能认为状态没有变化,跳过更新。
2.6 联合类型状态:加载状态
这个场景在 TypeScript 中很常见。
javascript
type LoadingStatus =
| "idle"
| "loading"
| "success"
| "error";
function DataLoader() {
const [status, setStatus] =
useState<LoadingStatus>("idle");
return (
<div>
<p>当前状态:{status}</p>
<button onClick={() => setStatus("loading")}>
开始加载
</button>
</div>
);
}
这样可以限制状态值:
scss
setStatus("success"); // ✅
setStatus("loading"); // ✅
setStatus("unknown"); // ❌
比直接使用 string 更安全,特别适合接口请求状态、任务执行状态。
三、useState 的两种更新方式
你可能注意到,前面的示例中出现了两种写法:
scss
setCount(count + 1);
以及:
ini
setCount(prev => prev + 1);
这两种有什么区别?
3.1 直接传入新值
scss
setCount(10);
表示直接把状态更新为 10。
适合明确知道新值的场景。
3.2 传入更新函数(推荐用于依赖旧状态的情况)
ini
setCount(prev => prev + 1);
其中 prev 表示 React 传入的前一个待更新状态。
例如:
scss
function handleClick() {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
}
点击一次,通常只增加 1。
因为这三个 count 都是当前这次渲染中的同一个值。
改成:
ini
function handleClick() {
setCount(prev => prev + 1);
setCount(prev => prev + 1);
setCount(prev => prev + 1);
}
点击一次,就增加 3。
原因是 React 会依次处理这些状态更新函数。
记住:如果新状态依赖旧状态,优先使用 prev => ...。
四、useState 的初始化方式
4.1 直接初始化
scss
const [count, setCount] = useState(0);
const [name, setName] = useState("张三");
这是最常用的方式。
4.2 惰性初始化
如果初始值需要复杂计算,可以传入一个函数:
scss
const [data, setData] = useState(() => {
return generateLargeData();
});
React 只在初始化状态时调用这个初始化函数,而不是每次渲染都调用。
注意,在开发模式 Strict Mode 下,React 可能会额外调用初始化函数,以检查是否存在不纯的逻辑。
五、useState 和 useEffect:如何获取后端数据?
这是业务开发中最典型的场景。
假设后端提供接口:
bash
GET /api/tasks
返回:
css
[ { "id": 1, "name": "视频回查", "status": "running" }, { "id": 2, "name": "文章回扫", "status": "pending" }]
React 示例:
typescript
import { useEffect, useState } from "react";
interface Task {
id: number;
name: string;
status: string;
}
function TaskList() {
const [tasks, setTasks] = useState<Task[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function fetchTasks() {
try {
const response = await fetch("/api/tasks");
if (!response.ok) {
throw new Error("请求失败");
}
const data: Task[] = await response.json();
setTasks(data);
} catch (error) {
console.error(error);
} finally {
setLoading(false);
}
}
fetchTasks();
}, []);
if (loading) {
return <div>加载中...</div>;
}
return (
<div>
{tasks.map(task => (
<div key={task.id}>
{task.name} - {task.status}
</div>
))}
</div>
);
}
这里的 Task[] 是对接口数据的类型标注,并不代表已经完成运行时数据校验。
重点理解三个概念:
useState:保存后端返回的数据。useEffect:组件挂载后执行请求等副作用。setTasks:数据返回后更新状态,触发页面重新渲染。
在真实项目中还需要考虑请求取消、错误展示和重复请求等问题。
六、useState 常见错误
错误一:直接修改状态
arduino
count++; // ❌
正确:
ini
setCount(prev => prev + 1);
错误二:直接修改对象或数组
ini
user.name = "李四"; // ❌
tasks.push(newTask); // ❌
正确:
ini
setUser(prev => ({
...prev,
name: "李四"
}));
setTasks(prev => [...prev, newTask]);
错误三:在 if 中调用 useState
javascript
function App({ visible }: { visible: boolean }) {
if (visible) {
const [count, setCount] = useState(0); // ❌
}
return <div>App</div>;
}
React Hook 必须在组件顶层调用,不能放在条件、循环或普通嵌套函数中。
正确:
javascript
function App({ visible }: { visible: boolean }) {
const [count, setCount] = useState(0);
if (!visible) {
return null;
}
return <div>{count}</div>;
}
错误四:以为 setState 后变量会立即变化
scss
function handleClick() {
setCount(count + 1);
console.log(count);
}
这里输出的仍是当前渲染对应的旧 count。
这是因为每次渲染中的状态值可以看作一份快照。
七、useState 常见用法总结
| 状态类型 | 初始化 | 更新 |
|---|---|---|
| 数字 | useState(0) |
setCount(c => c + 1) |
| 字符串 | useState("") |
setName("张三") |
| 布尔值 | useState(false) |
setOpen(v => !v) |
| 对象 | useState<User>(user) |
setUser(p => ({...p, name: "李四"})) |
| 数组 | useState<Task[]>([]) |
setTasks(p => [...p, task]) |
| 联合类型 | useState<Status>("idle") |
setStatus("loading") |
| 空值 | `useState<User | null>(null)` |
八、最后总结
学习 React 的 useState,最重要的是理解以下四句话:
useState用来保存组件状态。- 调用
setState更新状态,会让 React 安排重新渲染。 - 数组和对象状态不要直接修改,应创建新数组或新对象。
- 新状态依赖旧状态时,优先使用函数式更新
prev => ...。
你可以把 React 的数据渲染理解成下面这个公式:
\\text{UI} = f(\\text{State})
也就是:页面 UI 是状态 State 的映射结果。
状态变化,React 重新计算组件应该显示什么,再把需要变化的部分更新到页面。
这正是 React 声明式编程的核心思想。