React: State 基础:useState

最后更新:2026-08-26

State 是组件的 记忆。如果说 Props 是外人给你的参数(只读),那 State 就是你自己的小本本(可以改)。useState 就是 React 给组件装上的"记忆芯片"。


1. 你将学到



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)

多个组件需要共享同一个状态时,把状态提升到它们最近的共同父组件中。

100%
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
逻辑代码 49 行(超过 40 行限制,仅展示)

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(...))

📖 小节


📝 作业

  1. 基础题(难度⭐):创建一个 LikeButton 组件,点击切换 ❤️ / 🤍 状态,并显示点赞数。
  2. 进阶题(难度⭐⭐):创建一个 ExpenseTracker 组件,用 useState 管理支出列表(描述、金额、日期),支持添加和删除。
  3. 挑战题(难度⭐⭐⭐):创建一个 ColorPicker 组件,用 3 个 useState 管理 R/G/B 三个滑块值(0-255),实时显示颜色预览和十六进制色码。
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏