第 146 章:State 与 useState
学习目标
- 理解 State 与 Props 的区别
- 掌握
useState的基础用法 - 学会函数式更新和对象/数组 State
- 避免 State 常见反模式
一、State 是什么
State 是组件内部的、可变的、触发重新渲染的数据。
二、useState 基础
tsx
'use client';
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0); // ① 初始值 0
return (
<div>
<p>计数:{count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
<button onClick={() => setCount(0)}>重置</button>
</div>
);
}2.1 解构含义
tsx
const [count, setCount] = useState(0);
// ↑ 当前值 ↑ 更新函数2.2 多次调用
tsx
const [name, setName] = useState('Tom');
const [age, setAge] = useState(18);
const [list, setList] = useState<User[]>([]);三、State 更新规则
3.1 不可变更新
tsx
// ✅ 替换整个值
setCount(count + 1);
setName('Jerry');
// ❌ 直接修改
count++; // 错
count = 5; // 错3.2 函数式更新(基于旧值)
tsx
// ❌ 异步问题:多次连续 setCount 拿到同一个旧值
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
// 最终 count = 1,不是 3
// ✅ 函数式:每次拿到最新值
setCount(c => c + 1);
setCount(c => c + 1);
setCount(c => c + 1);
// 最终 count = 3四、对象 State
tsx
interface User {
name: string;
age: number;
}
const [user, setUser] = useState<User>({ name: 'Tom', age: 18 });
// ❌ 直接修改
user.name = 'Jerry';
setUser(user); // 不会触发渲染
// ✅ 展开返回新对象
setUser({ ...user, name: 'Jerry' });
// ✅ 多个字段更新
setUser(prev => ({ ...prev, name: 'Jerry', age: 19 }));五、数组 State
tsx
const [list, setList] = useState<string[]>([]);
// 追加
setList([...list, 'new']);
// 头部插入
setList(['new', ...list]);
// 删除
setList(list.filter(x => x !== 'target'));
// 更新某项
setList(list.map(x => x === 'old' ? 'new' : x));
// 按索引删除
setList(list.filter((_, i) => i !== index));六、State 提升
当多个组件需要共享 state,把它提升到最近的共同父组件。
tsx
function Parent() {
const [count, setCount] = useState(0);
return (
<>
<ChildA count={count} />
<ChildB onIncrement={() => setCount(c => c + 1)} />
</>
);
}七、useState 的陷阱
7.1 初始值只算一次
tsx
// ❌ 每次渲染都执行 expensiveFunc
const [data, setData] = useState(expensiveFunc());
// ✅ 用函数,只在第一次执行
const [data, setData] = useState(() => expensiveFunc());7.2 对象/数组引用
tsx
// ❌ 即使值相同,引用变化也会触发渲染
const [config, setConfig] = useState({ theme: 'dark' });
// 父组件传新对象,即使内容一样,子组件也会重渲染
// ✅ 用 useMemo 稳定引用(子组件用 memo)7.3 不要在条件中使用
tsx
// ❌ 错
if (loggedIn) {
const [user, setUser] = useState(null);
}
// ✅ 始终在最顶层调用
const [user, setUser] = useState(null);
if (loggedIn) { /* 用 user */ }八、常见场景
8.1 表单输入
tsx
const [email, setEmail] = useState('');
return <input value={email} onChange={e => setEmail(e.target.value)} />;8.2 切换状态
tsx
const [open, setOpen] = useState(false);
return (
<div>
<button onClick={() => setOpen(!open)}>切换</button>
{open && <Modal />}
</div>
);8.3 异步加载
tsx
const [data, setData] = useState<User | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
setLoading(true);
fetch('/api/user')
.then(r => r.json())
.then(setData)
.catch(setError)
.finally(() => setLoading(false));
}, []);九、本章小结
| 概念 | 关键 |
|---|---|
| useState | 返回 [值, 设置函数] |
| 不可变更新 | 永远返回新值,不修改原值 |
| 函数式更新 | setX(prev => ...) |
| 对象/数组 | 展开运算符返回新对象 |
| 提升 | 多个组件共享时提升到共同父组件 |
动手练习
- 写一个 Todo 组件,能添加、删除、勾选
- 写一个表单,包含 name/email/age 字段
- 写一个分页组件,显示当前页和总页数
- 重构一个用 props 传状态的组件,用 useState
推荐阅读
下一章:第 147 章:条件渲染与列表 →