跳至正文
来两杯美式
返回

React 核心(4):useState 完全指南

By 来两杯美式
发布于

一、useState 做的事就一件:给组件一个”记忆”

没有 useState 的组件是无记忆的——每次渲染都是全新计算,函数里的变量全部重置。有了 useState,组件能记住上一次的值,并且改了之后自动触发重渲染。

function Counter() {
  const [count, setCount] = useState(0);
  //     ↑ 当前值   ↑ 改值的函数    ↑ 初始值

  return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

useState(0) 返回了什么?

它返回一个长度为 2 的数组

const result = useState(0);
// result[0] → 当前值(第一次渲染是 0)
// result[1] → 修改值的函数

我们用解构赋值给这两个东西取了名字:

const [count, setCount] = useState(0);
// 等价于:
// const count = result[0]
// const setCount = result[1]

名字随你取,但有个约定俗成的规则:值叫 X,改值的函数叫 setX

const [name, setName] = useState(""); // name / setName
const [isOpen, setIsOpen] = useState(false); // isOpen / setIsOpen
const [items, setItems] = useState([]); // items / setItems

setCount 是什么?

setCount 是 React 给你的一个函数。你调用它,传入一个新值,React 就会:

  1. count 更新为你传的值
  2. 自动触发组件重新渲染
setCount(5); // 你调用 → React 把 count 改成 5 → 组件重新渲染
setCount(10); // 你调用 → React 把 count 改成 10 → 组件重新渲染

你不需要知道 setCount 内部怎么实现——你只需要知道:调了它,值就变了,页面就自动更新了

你可以把它理解成一个”遥控器”:你按一下(调用),React 就帮你改值 + 刷新页面。你不用自己动手改 DOM。

二、它和声明式的关系

useState 是 React 声明式链条的触发器

setCount(1)           ← 你做的事:调 setter 改状态
  → React 知道状态变了
    → 重新执行组件函数,拿到新的 Virtual DOM
      → Diff → 更新真实 DOM

你只管”数据变成什么”(setCount(1)),不用管”页面怎么跟着变”(React 自动处理)。这就是声明式——详见 声明式与 React 的实现机制

useState 不是声明式本身,而是声明式链条的起点。你拿 useState 一样可以写命令式代码(存 DOM ref 然后手动操作),只是没人会这么干。同一层的其他 API 如 useReduceruseContext 也是触发器——它们共享同一个”调了就触发重渲染”的机制。

三、更新机制

3.1 直接传值 vs 传函数

setCountuseState 返回的第二个值——一个由 React 提供的函数,用来修改状态并触发重渲染。它接受两种参数:

// 直接传值:你告诉它"新值是什么"
setCount(5); // count 变成 5,不管之前是多少

// 传函数:你告诉它"怎么从旧值算出新值"
setCount(prev => prev + 1); // 在旧值基础上 +1
//     ↑ prev 是当前最新的 count 值
//       ↑ 返回值是新值

类比:直接传值像跟快递员说”把包裹放在门口”——具体指令;传函数像说”包裹比昨天多放一个”——规则,不是具体数字。

什么时候必须用函数形式?当新值依赖旧值时

// ❌ 连续调用时,两次 count 可能读到同一个旧值
setCount(count + 1); // 假设 count=0,两次都算出 1
setCount(count + 1); // 结果是 1,不是 2

// ✅ 函数形式保证每次拿到最新值
setCount(prev => prev + 1); // 0→1
setCount(prev => prev + 1); // 1→2,结果是 2

原因:React 会批量处理状态更新——多次 setState 可能被合并成一次渲染,直接传值时 count 是当前渲染闭包里的旧值,不是最新的。

简单判断:新值需要用到旧值 → 用函数形式;不需要 → 直接传值。

3.2 更新是”替换”不是”合并”

useState 的 setter 不会自动合并对象——它直接替换整个值:

const [user, setUser] = useState({ name: "yi", age: 25 });

// ❌ 直接改属性:React 检测不到变化(引用没变)
user.age = 26;
setUser(user);

// ❌ 只传部分字段:name 丢失了!
setUser({ age: 26 });

// ✅ 展开旧值再覆盖
setUser({ ...user, age: 26 });
// 等价于 { name: 'yi', age: 26 }

这和 class 组件的 this.setState({ age: 26 }) 不同——this.setState 会自动浅合并,useState 的 setter 不会。

3.3 引用类型与不可变性

对象和数组的更新必须创建新引用,React 才能检测到变化:

// 对象
setUser({ ...user, age: 26 }); // ✅ 新对象
setUser(Object.assign({}, user, { age: 26 })); // ✅ 新对象

// 数组
setItems([...items, newItem]); // ✅ 加一项(新数组)
setItems(items.filter(i => i.id !== id)); // ✅ 删一项(新数组)
setItems(items.map(i => (i.id === id ? { ...i, done: true } : i))); // ✅ 改一项
// ❌ 这些操作修改了原数组/原对象,React 检测不到变化
items.push(newItem);
setItems(items);
items.splice(0, 1);
setItems(items);
user.age = 26;
setUser(user);

原则:永远不要直接修改 state,总是创建新值。这就是 React 强调的”不可变性(Immutability)”。

四、常见用法模式

4.1 开关 / Toggle

const [isOpen, setIsOpen] = useState(false)

<button onClick={() => setIsOpen(!isOpen)}>Toggle</button>
// 更安全的写法(避免闭包陷阱):
<button onClick={() => setIsOpen(prev => !prev)}>Toggle</button>

4.2 表单输入

const [name, setName] = useState('')

<input value={name} onChange={e => setName(e.target.value)} />

多个表单字段用对象管理:

const [form, setForm] = useState({ name: '', email: '' })

<input
  value={form.name}
  onChange={e => setForm({ ...form, name: e.target.value })}
/>
<input
  value={form.email}
  onChange={e => setForm({ ...form, email: e.target.value })}
/>

4.3 列表

const [items, setItems] = useState([]);

// 加
const addItem = item => setItems(prev => [...prev, item]);

// 删
const removeItem = id => setItems(prev => prev.filter(i => i.id !== id));

// 改
const toggleItem = id =>
  setItems(prev => prev.map(i => (i.id === id ? { ...i, done: !i.done } : i)));

4.4 惰性初始化

当初始值需要昂贵计算时,传函数避免每次渲染都重新算:

// ❌ 每次渲染都执行 createInitialItems(),哪怕只用第一次
const [items, setItems] = useState(createInitialItems());

// ✅ 只在首次渲染时执行
const [items, setItems] = useState(() => createInitialItems());

useState 接收函数时,只在初始化时调用一次;接收值时,每次渲染都会先计算这个值(然后被丢弃)。

TanStack Query 的 const [qc] = useState(() => new QueryClient()) 就是这个模式——new QueryClient() 只执行一次,后续渲染复用同一个实例。

五、易错点

5.1 闭包陷阱:读到了旧的 state

function Counter() {
  const [count, setCount] = useState(0);

  function handleClick() {
    // 这里的 count 是渲染时的快照,不是"最新值"
    console.log(count); // 可能是旧的
    setCount(count + 1);
  }

  // 3 秒后拿到的 count 是点击那一刻的值,不是当前值
  function handleDelay() {
    setTimeout(() => {
      setCount(count + 1); // ❌ 用的是闭包里的旧 count
    }, 3000);
  }

  // ✅ 用函数形式拿到最新值
  function handleDelayFix() {
    setTimeout(() => {
      setCount(prev => prev + 1); // ✅
    }, 3000);
  }
}

规律:只要”新值依赖旧值”且存在异步/延迟,就用函数形式 prev => ...

5.2 在条件语句里调用 Hook

// ❌ Hook 调用顺序不能变,React 靠调用顺序匹配 state
if (condition) {
  const [x, setX] = useState(0);
}

// ✅ 条件判断写在 Hook 内部
const [x, setX] = useState(0);
if (condition) {
  // 用不用 x 是你的事,但 Hook 必须每次都调用
}

这是 React Hook 的铁律:Hook 必须在组件顶层调用,不能在条件、循环、嵌套函数中调用。原因:React 按调用顺序匹配 state——第一次渲染 useState 调了 3 次,第二次也必须调 3 次,顺序还一样,否则 state 就错位了。

5.3 setter 传相同的值不会触发重渲染

setCount(5); // 如果 count 已经是 5,React 会跳过这次渲染

React 用 Object.is() 比较新旧值,相同则不渲染。这对性能是好事——不需要你手动去防抖。

六、一句话

useState 给组件添加记忆,你调 setter 改状态,React 自动重渲染。记住三件事:新值依赖旧值用函数形式、对象/数组必须创建新引用、Hook 必须在顶层调用


分享这篇文章:
通过邮件分享这篇文章✓ 链接已复制
所属专题
React
第 4 / 17 篇
查看系列全部文章
  1. 01.React 核心(1):React 组件与函数的本质区别
  2. 02.React 核心(2):声明式与 React 的实现机制
  3. 03.React 核心(3):Props 完全指南
  4. 04.React 核心(4):useState 完全指南
  5. 05.React 核心(5):useEffect 完全指南
  6. 06.React 核心(6):useRef 完全指南
  7. 07.React 核心(7):Context API 与状态共享
  8. 08.React 核心(8):useCallback 与 useMemo
  9. 09.React 核心(9):Hooks 规则与闭包陷阱
  10. 10.React 核心(10):自定义 Hook 设计
  11. 11.React 核心(11):Key 与列表渲染
  12. 12.React 核心(12):表单与受控/非受控组件
  13. 13.React 核心(13):React 渲染机制深入——Fiber 与调度
  14. 14.React 核心(14):React 18 并发特性
  15. 15.React 核心(15):Suspense 机制详解
  16. 16.React 核心(16):TanStack Query 入门指南
  17. 17.React 核心(17):TanStack Query 缓存机制详解

上一篇
React 核心(5):useEffect 完全指南
下一篇
React 核心(3):Props 完全指南