React: State 基础:useState
最后更新:2026-08-26
State 是组件的 记忆。如果说 Props 是外人给你的参数(只读),那 State 就是你自己的小本本(可以改)。useState 就是 React 给组件装上的"记忆芯片"。
1. 你将学到
- 什么是 State,为什么需要 State
- useState 的基本用法
- 数组和对象状态的正确更新方式(不可变性)
- 多个 State 变量的管理
- 状态提升(Lifting State Up)
2. 一个计数器引发的思考
(1) 痛点:普通变量不会触发 UI 更新
Bob 想做一个简单的计数器:点击按钮,数字 +1。
JSX
// ❌ 普通变量,不会触发重新渲染
function Counter() {
let count = 0 // 普通变量
function handleClick() {
count = count + 1 // 变量变了,但 UI 不会更新!
console.log(count) // 控制台显示 1, 2, 3... 但页面上永远显示 0
}
return (
<div>
<p>计数:{count}</p> {/* 永远显示 0 */}
<button onClick={handleClick}>+1</button>
</div>
)
}
Bob 发现问题了:普通变量不会通知 React 更新 UI。每次组件重新渲染时,count 都会被重置为 0。
(2) useState 的解法
JSX
// ✅ 使用 State,React 会自动追踪变化并更新 UI
import { useState } from 'react'
function Counter() {
const [count, setCount] = useState(0) // 初始值 0
function handleClick() {
setCount(count + 1) // 更新状态 → React 自动重新渲染
}
return (
<div>
<p>计数:{count}</p> {/* 每次点击自动更新 */}
<button onClick={handleClick}>+1</button>
</div>
)
}
收益:useState 返回一个数组 [当前值, 更新函数]。调用 setCount 后,React 会自动重新渲染组件,显示最新的值。
3. useState 的核心概念
(1) 解构赋值
JSX
import { useState } from 'react'
// useState(initialValue) 返回 [value, setValue]
const [count, setCount] = useState(0)
// ^ ^ ^
// | | └─ 初始值(只在首次渲染生效)
// | └─ 更新函数(调用后触发重新渲染)
// └─ 当前状态值(每次渲染获取最新值)
| 部分 | 说明 |
|---|---|
count |
当前状态值,每次渲染都拿到最新值 |
setCount |
更新函数,调用它 → React 重新渲染组件 |
useState(0) |
初始值(首次渲染时使用,后续渲染忽略) |
(2) 更新机制
JSX
function Example() {
const [count, setCount] = useState(0)
function handleClick() {
setCount(count + 1) // 1. 请求更新
// 注意:这里 count 还是旧值!
console.log(count) // 输出 0,不是 1
}
return <button onClick={handleClick}>{count}</button>
}
关键理解:
setCount(count + 1)是"请求 React 在下次渲染时更新 count",而不是"立即修改 count"。这叫做异步更新。
(3) 函数式更新
如果新状态依赖旧状态,应该用函数式更新:
JSX
function Counter() {
const [count, setCount] = useState(0)
function handleClick() {
// ✅ 推荐:函数式更新
setCount(prev => prev + 1)
setCount(prev => prev + 1) // 连续调用两次,count 会 +2
}
// ❌ 不推荐:直接更新(连续调用只生效一次)
function handleBadClick() {
setCount(count + 1)
setCount(count + 1) // 两次调用的 count 都是旧值,结果只 +1
}
return <button onClick={handleClick}>当前:{count}</button>
}
| 方式 | 写法 | 特点 |
|---|---|---|
| 直接更新 | setCount(count + 1) |
简单,但连续调用会丢失 |
| 函数式更新 | setCount(prev => prev + 1) |
准确,连续调用也正确 |
4. 数组和对象的更新(不可变性)
React 要求状态不可直接修改。永远不要做 state.push() 或 state.name = 'xxx',要创建新数组/新对象替换旧值。
(1) 更新对象
JSX
function UserEditor() {
const [user, setUser] = useState({
name: 'Alice',
age: 28,
email: 'alice@example.com'
})
function updateName(newName) {
// ❌ 错误:直接修改 state 对象
user.name = newName
// React 不会检测到变化,UI 不会更新
// ✅ 正确:创建一个新对象
setUser({ ...user, name: newName })
}
function updateAge(newAge) {
// ✅ 展开运算符:保留其他字段,只更新 age
setUser({ ...user, age: newAge })
}
function resetUser() {
// ✅ 重置为初始值
setUser({ name: '', age: 0, email: '' })
}
return (
<div>
<p>{user.name} - {user.age} 岁</p>
<button onClick={() => updateName('Bob')}>改名 Bob</button>
<button onClick={() => updateAge(user.age + 1)}>年龄 +1</button>
</div>
)
}
(2) 更新数组
JSX
function ShoppingCart() {
const [items, setItems] = useState([
{ id: 1, name: '苹果', qty: 2 },
{ id: 2, name: '香蕉', qty: 1 }
])
// 添加:用展开运算符创建新数组
function addItem(name) {
setItems([...items, { id: Date.now(), name, qty: 1 }])
}
// 删除:用 filter 创建新数组
function removeItem(id) {
setItems(items.filter(item => item.id !== id))
}
// 更新:用 map 创建新数组
function updateQty(id, newQty) {
setItems(items.map(item =>
item.id === id ? { ...item, qty: newQty } : item
))
}
return (
<div>
<button onClick={() => addItem('橙子')}>添加橙子</button>
<ul>
{items.map(item => (
<li key={item.id}>
{item.name} × {item.qty}
<button onClick={() => updateQty(item.id, item.qty + 1)}>+</button>
<button onClick={() => removeItem(item.id)}>删除</button>
</li>
))}
</ul>
</div>
)
}
▶ 示例:数组操作速查表
JSX
// ============================================
// 示例:React 中数组操作的 6 种常见场景
// ============================================
const [arr, setArr] = useState([1, 2, 3])
// 1. 追加到末尾
setArr([...arr, 4]) // [1, 2, 3, 4]
// 2. 追加到开头
setArr([0, ...arr]) // [0, 1, 2, 3]
// 3. 插入到中间
const insertAt = 1
setArr([...arr.slice(0, insertAt), 99, ...arr.slice(insertAt)])
// [1, 99, 2, 3]
// 4. 删除元素(filter)
setArr(arr.filter(n => n !== 2)) // [1, 3]
// 5. 更新元素(map)
setArr(arr.map(n => n === 2 ? 22 : n)) // [1, 22, 3]
// 6. 排序(先复制再排序)
setArr([...arr].sort((a, b) => b - a)) // [3, 2, 1]
5. 多个 State 变量
一个组件可以有多个 State 变量,建议按逻辑拆分,而不是塞进一个大对象:
JSX
function RegistrationForm() {
// ✅ 推荐:按逻辑拆分
const [name, setName] = useState('')
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [isSubmitting, setIsSubmitting] = useState(false)
const [errors, setErrors] = useState({})
// ❌ 不推荐:所有状态塞进一个对象
const [form, setForm] = useState({
name: '', email: '', password: '',
isSubmitting: false, errors: {}
})
// 更新时要展开整个对象,容易漏掉字段
}
| 策略 | 优点 | 缺点 |
|---|---|---|
| 分开多个 useState | 更新精确、类型安全、易于阅读 | 状态多时变量多 |
| 合在一个 useState | 一次性更新多个字段方便 | 更新语法繁琐,容易忘展开 |
建议:独立的状态(输入框、加载标志、错误信息)分开用多个 useState;逻辑上相关的字段(如用户信息的所有字段)可以合并为一个对象。
6. 状态提升(Lifting State Up)
当多个组件需要共享同一个状态时,把状态提升到它们最近的共同父组件中。
graph TB
subgraph "状态提升前(数据不互通)"
A[父组件 App] --- B[子组件 A<br/>有自己的 count]
A --- C[子组件 B<br/>有自己的 count]
end
subgraph "状态提升后(数据共享)"
D[父组件 App<br/>**count 在这里**] --- E[子组件 A<br/>读取 count, 调用 setCount]
D --- F[子组件 B<br/>读取 count, 调用 setCount]
end
▶ 示例:状态提升实战
JSX
📖 仅展示
// ============================================
// 示例:温度转换器(状态提升)
// 功能:摄氏度和华氏度互相转换,共享同一个温度值
// ============================================
// ---- 子组件:摄氏温度输入 ----
function CelsiusInput({ celsius, onCelsiusChange }) {
return (
<div>
<label>摄氏温度(°C):</label>
<input
value={celsius}
onChange={e => onCelsiusChange(e.target.value)}
style={{ margin: '8px', padding: '4px' }}
/>
</div>
)
}
// ---- 子组件:华氏温度输入 ----
function FahrenheitInput({ fahrenheit, onFahrenheitChange }) {
return (
<div>
<label>华氏温度(°F):</label>
<input
value={fahrenheit}
onChange={e => onFahrenheitChange(e.target.value)}
style={{ margin: '8px', padding: '4px' }}
/>
</div>
)
}
// ---- 父组件:状态在这里管理 ----
function TemperatureConverter() {
// 状态提升到共同父组件
const [temperature, setTemperature] = useState('')
function handleCelsiusChange(value) {
setTemperature(value) // 存摄氏温度
// 不需要两个状态!只存一个,另一个由公式算出
}
function handleFahrenheitChange(value) {
// 华氏 → 摄氏:°C = (°F - 32) × 5/9
setTemperature(value ? ((parseFloat(value) - 32) * 5 / 9).toFixed(1) : '')
}
const celsius = temperature
const fahrenheit = temperature
? (parseFloat(temperature) * 9 / 5 + 32).toFixed(1)
: ''
return (
<div style={{ padding: '20px', border: '1px solid #ddd', borderRadius: '8px' }}>
<h2>温度转换器</h2>
<CelsiusInput celsius={celsius} onCelsiusChange={handleCelsiusChange} />
<FahrenheitInput fahrenheit={fahrenheit} onFahrenheitChange={handleFahrenheitChange} />
{temperature && (
<p style={{ color: '#666', marginTop: '12px' }}>
{celsius}°C = {fahrenheit}°F
</p>
)}
</div>
)
}
// 在摄氏输入框输入 100 → 华氏自动显示 212°F
// 在华氏输入框输入 212 → 摄氏自动显示 100°C
7. 完整示例:购物车计数器
JSX
// ============================================
// 完整示例:购物车(useState 综合运用)
// 功能:添加商品、增减数量、删除、总价计算
// ============================================
import { useState } from 'react'
function ShoppingCart() {
// 多个 State 变量,按逻辑拆分
const [items, setItems] = useState([
{ id: 1, name: 'React 实战教程', price: 89, qty: 1 },
{ id: 2, name: 'TypeScript 入门', price: 59, qty: 2 }
])
const [discountCode, setDiscountCode] = useState('')
const [appliedDiscount, setAppliedDiscount] = useState(0)
// 增加数量(函数式更新确保准确)
function increment(id) {
setItems(items.map(item =>
item.id === id ? { ...item, qty: item.qty + 1 } : item
))
}
// 减少数量(不低于 1)
function decrement(id) {
setItems(items.map(item =>
item.id === id ? { ...item, qty: Math.max(1, item.qty - 1) } : item
))
}
// 删除商品
function remove(id) {
setItems(items.filter(item => item.id !== id))
}
// 应用折扣码
function applyDiscount() {
if (discountCode === 'REACT2026') {
setAppliedDiscount(20) // 满 100 减 20
} else {
alert('折扣码无效')
}
}
// 计算总价
const subtotal = items.reduce((sum, item) => sum + item.price * item.qty, 0)
const total = Math.max(0, subtotal - appliedDiscount)
return (
<div style={{ maxWidth: '600px', margin: '0 auto' }}>
<h2>🛒 购物车</h2>
{/* 商品列表 */}
{items.length === 0 ? (
<p style={{ color: '#999', textAlign: 'center', padding: '40px' }}>
购物车是空的,快去逛逛吧!
</p>
) : (
items.map(item => (
<div key={item.id} style={{
display: 'flex', alignItems: 'center',
padding: '12px', borderBottom: '1px solid #f0f0f0'
}}>
<div style={{ flex: 1 }}>
<h4 style={{ margin: 0 }}>{item.name}</h4>
<p style={{ margin: '4px 0', color: '#ff4d4f' }}>${item.price}</p>
</div>
{/* 数量控制 */}
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<button onClick={() => decrement(item.id)} style={btnStyle}>−</button>
<span>{item.qty}</span>
<button onClick={() => increment(item.id)} style={btnStyle}>+</button>
</div>
{/* 小计 */}
<p style={{ margin: '0 16px', fontWeight: 'bold', width: '80px', textAlign: 'right' }}>
${item.price * item.qty}
</p>
{/* 删除 */}
<button onClick={() => remove(item.id)} style={{ ...btnStyle, backgroundColor: '#ff4d4f', color: 'white' }}>
✕
</button>
</div>
))
)}
{/* 折扣码 */}
<div style={{ marginTop: '16px', display: 'flex', gap: '8px' }}>
<input
value={discountCode}
onChange={e => setDiscountCode(e.target.value)}
placeholder="输入折扣码"
style={{ flex: 1, padding: '8px', border: '1px solid #d9d9d9', borderRadius: '4px' }}
/>
<button onClick={applyDiscount} style={{
padding: '8px 16px', backgroundColor: '#52c41a', color: 'white',
border: 'none', borderRadius: '4px', cursor: 'pointer'
}}>
应用
</button>
</div>
{/* 价格汇总 */}
<div style={{ marginTop: '16px', padding: '16px', backgroundColor: '#fafafa', borderRadius: '8px' }}>
<p>小计:${subtotal}</p>
{appliedDiscount > 0 && <p style={{ color: '#52c41a' }}>优惠:-${appliedDiscount}</p>}
<p style={{ fontSize: '20px', fontWeight: 'bold' }}>合计:${total}</p>
</div>
</div>
)
}
const btnStyle = {
width: '32px', height: '32px',
border: '1px solid #d9d9d9', borderRadius: '4px',
backgroundColor: 'white', cursor: 'pointer',
fontSize: '16px', display: 'flex', alignItems: 'center',
justifyContent: 'center'
}
交互流程:
- 点 +/− 调整数量,小计和合计实时更新
- 点 ✕ 删除商品,空购物车时显示友好提示
- 输入折扣码
REACT2026可减 20 元
▶ 示例 3:对象状态更新——用户信息编辑
JSX
function UserEditor() {
const [user, setUser] = useState({ name: 'Alice', email: 'alice@test.com', age: 28 })
function updateField(field, value) {
setUser(prev => ({ ...prev, [field]: value }))
}
return (
<div style={{ maxWidth: 400, margin: '0 auto' }}>
<h3>Edit Profile</h3>
<input value={user.name} onChange={e => updateField('name', e.target.value)}
placeholder="Name" style={{ width: '100%', padding: 8, marginBottom: 8, borderRadius: 4 }} />
<input value={user.email} onChange={e => updateField('email', e.target.value)}
placeholder="Email" style={{ width: '100%', padding: 8, marginBottom: 8, borderRadius: 4 }} />
<input type="number" value={user.age} onChange={e => updateField('age', Number(e.target.value))}
placeholder="Age" style={{ width: '100%', padding: 8, marginBottom: 8, borderRadius: 4 }} />
<pre style={{ background: '#f5f5f5', padding: 12, borderRadius: 4, fontSize: 13 }}>
{JSON.stringify(user, null, 2)}
</pre>
</div>
)
}
▶ 示例 4:嵌套对象状态——地址管理
JSX
function AddressForm() {
const [address, setAddress] = useState({
street: '', city: '', zip: '',
country: 'US', isPrimary: true,
})
function update(path, value) {
setAddress(prev => ({ ...prev, [path]: value }))
}
return (
<div style={{ maxWidth: 400, margin: '0 auto' }}>
<h3>Shipping Address</h3>
<input value={address.street} onChange={e => update('street', e.target.value)}
placeholder="Street" style={{ width: '100%', padding: 8, marginBottom: 8, borderRadius: 4 }} />
<div style={{ display: 'flex', gap: 8, marginBottom: 8 }}>
<input value={address.city} onChange={e => update('city', e.target.value)}
placeholder="City" style={{ flex: 2, padding: 8, borderRadius: 4 }} />
<input value={address.zip} onChange={e => update('zip', e.target.value)}
placeholder="ZIP" style={{ flex: 1, padding: 8, borderRadius: 4 }} />
</div>
<select value={address.country} onChange={e => update('country', e.target.value)}
style={{ width: '100%', padding: 8, marginBottom: 8, borderRadius: 4 }}>
<option value="US">United States</option>
<option value="CN">China</option>
<option value="JP">Japan</option>
</select>
<label>
<input type="checkbox" checked={address.isPrimary}
onChange={e => update('isPrimary', e.target.checked)} /> Primary address
</label>
</div>
)
}
▶ 示例 5:函数式更新解决批量更新问题
JSX
function ScoreBoard() {
const [score, setScore] = useState(0)
const [multiplier, setMultiplier] = useState(1)
function addPoints(base) {
setScore(prev => prev + base * multiplier)
}
function resetScore() {
setScore(0)
setMultiplier(1)
}
function doubleMultiplier() {
setMultiplier(prev => Math.min(prev * 2, 8))
}
return (
<div style={{ maxWidth: 300, margin: '0 auto', textAlign: 'center' }}>
<h3>Score: {score}</h3>
<p>Multiplier: x{multiplier}</p>
<div style={{ display: 'flex', gap: 8, justifyContent: 'center', marginBottom: 8 }}>
<button onClick={() => addPoints(10)} style={{ padding: '8px 16px', cursor: 'pointer' }}>+10 pts</button>
<button onClick={() => addPoints(50)} style={{ padding: '8px 16px', cursor: 'pointer' }}>+50 pts</button>
<button onClick={() => addPoints(100)} style={{ padding: '8px 16px', cursor: 'pointer' }}>+100 pts</button>
</div>
<div style={{ display: 'flex', gap: 8, justifyContent: 'center' }}>
<button onClick={doubleMultiplier} style={{ padding: '8px 16px', cursor: 'pointer' }}>2x Multiplier</button>
<button onClick={resetScore} style={{ padding: '8px 16px', cursor: 'pointer' }}>Reset</button>
</div>
</div>
)
}
❓ 常见问题
Q useState 和普通变量的性能有什么区别?
A useState 有额外开销(追踪变化、调度渲染),所以只在需要"变化时触发 UI 更新"的场景使用。如果变量变化不需要更新 UI(如定时器 ID、滚动位置记录),用
useRef(第 10 课)更合适。Q 一个组件里可以有多少个 useState?有性能问题吗?
A 没有数量限制。React 官方建议按逻辑拆分为多个 useState 而不是一个大对象。每个 useState 调用就是一个"状态单元",React 能高效处理大量 useState。常见组件通常有 3-8 个 useState。
Q setState 更新是同步还是异步的?
A 在 React 18 之前,setState 在事件处理函数中是"批量异步"的,在 setTimeout/Promise 中是同步的。React 18 引入了自动批量处理(Automatic Batching),无论在事件处理、setTimeout、Promise 还是 fetch 回调中,setState 都是批量异步的。如果确实需要同步获取更新后的 DOM,可以用
flushSync(() => setState(...))。📖 小节
- State 是组件的"记忆",变化时自动触发 UI 更新
useState(initialValue)返回[value, setValue]数组- 不可变性:永远不要直接修改 state,要创建新对象/新数组替换
- 函数式更新
setCount(prev => prev + 1)在依赖旧值时更准确 - 状态提升:多个子组件需要共享数据时,状态提到共同父组件
📝 作业
- 基础题(难度⭐):创建一个
LikeButton组件,点击切换 ❤️ / 🤍 状态,并显示点赞数。 - 进阶题(难度⭐⭐):创建一个
ExpenseTracker组件,用 useState 管理支出列表(描述、金额、日期),支持添加和删除。 - 挑战题(难度⭐⭐⭐):创建一个
ColorPicker组件,用 3 个 useState 管理 R/G/B 三个滑块值(0-255),实时显示颜色预览和十六进制色码。