Skip to content
第 4 章 前端 ⏱ 14 分钟阅读

第 4 章:State ​

学习目标 ​

  • 理解 State 是组件内部的"记忆"
  • 掌握 useState 的 4 种用法
  • 理解 State 不可变的概念
  • 避开 State 的 5 个常见坑

一、State 是什么? ​

State = 组件内部会变化的数据。改 state → 组件自动重渲染。

tsx
function Counter() {
  const [count, setCount] = useState(0);   // 初始值 0

  return (
    <div>
      <p>点击次数:{count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  );
}

要点:

  • useState 返回 [值, 更新函数]
  • 更新函数触发重渲染
  • 多次调用会合并(React 18 batching)

二、State vs Props ​

维度StateProps
来源组件内部声明父组件传入
可修改✅(通过 setter)❌(只读)
影响范围当前组件 + 子组件仅当前组件 + 子组件(只读)
触发重渲染改值即触发父组件改值传下来才触发

三、useState 的 4 种用法 ​

1. 数字 ​

tsx
const [count, setCount] = useState(0);
setCount(count + 1);             // 设新值
setCount(prev => prev + 1);      // ✅ 函数式更新,推荐

2. 字符串 ​

tsx
const [name, setName] = useState('');
setName('张三');

3. 布尔 ​

tsx
const [loading, setLoading] = useState(false);
setLoading(true);

4. 对象/数组 ​

tsx
const [user, setUser] = useState({ name: '', age: 18 });
const [list, setList] = useState<string[]>([]);

// �️ 不能直接改原对象,要创建新对象
setUser({ ...user, name: '张三' });    // ✅ 创建新对象
user.name = '张三';                     // ❌ 直接改不会重渲染

⚠️ 坑 1:State 是不可变的(immutable),改值必须新建对象/数组,React 靠引用变化判断要不要重渲染。

四、函数式更新(异步陷阱) ​

多次连续 setCount(count + 1) 在异步环境下可能只生效一次:

tsx
function handleClick() {
  setCount(count + 1);   // count = 0,加 1 = 1
  setCount(count + 1);   // count 还是 0,加 1 = 1(不是 2!)
  setCount(count + 1);   // 还是 1
}

正确写法:函数式更新,参数是上一个 state:

tsx
function handleClick() {
  setCount(prev => prev + 1);   // 0 + 1 = 1
  setCount(prev => prev + 1);   // 1 + 1 = 2
  setCount(prev => prev + 1);   // 2 + 1 = 3
}

闭包陷阱的本质 ​

count 是 render 时拍下来的"快照",写死在 handler 里就是固定值——三次 setCount(count + 1) 用的都是同一个旧值 0。

写法React 收到啥
setCount(count + 1)一个固定数字(0 + 1 = 1,写死的)
setCount(prev => prev + 1)一个函数(让 React 自己查最新值再算)

prev 不是闭包变量,是 React 内部维护的"上一步真实结果"——每次都基于上一步算。

什么时候用哪种写法 ​

写法何时用
setX(prev => ...) 函数式新值依赖旧值
setX(新值) 普通新值跟旧值无关

用函数式的 5 个典型场景:

tsx
// ① 累加/累减
setCount(prev => prev + 1)

// ② 连续 toggle boolean
setOpen(prev => !prev)

// ③ 基于旧数组筛选/追加/删除
setList(prev => prev.filter(x => x.id !== id))
setList(prev => [...prev, newItem])

// ④ 对象里改单个字段
setForm(prev => ({ ...prev, name: '张三' }))

// ⑤ 异步回调里改 state(必须!)
setTimeout(() => {
  setCount(prev => prev + 1) // ← 异步里闭包陷阱更明显
}, 1000)

用普通写法的场景:

tsx
// ① 改成一个固定值(重置)
setCount(0)
setName('张三')

// ② 从外部拿来的值(不依赖旧 state)
setName(inputValue) // input 框直接绑定
setData(res.data)   // 接口返回的

// ③ 多个独立状态
setName('zs')
setAge(20)

�️ 坑 2:依赖上一个 state 时必须用函数式更新,否则连续多次调用只生效最后一次。

五、对象 State 的正确改法 ​

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

// ✅ 改单个字段
setForm({ ...form, name: '张三' });

// ✅ 函数式
setForm(prev => ({ ...prev, email: 'a@b.com' }));

// ❌ 直接改原对象,不会重渲染
form.name = '张三';

简化套路:

tsx
function update<K extends keyof Form>(key: K, value: Form[K]) {
  setForm(prev => ({ ...prev, [key]: value }));
}

// 使用
update('name', '张三');
update('email', 'a@b.com');

六、数组 State 的正确改法 ​

tsx
const [todos, setTodos] = useState<Todo[]>([]);

// 追加
setTodos([...todos, { id: Date.now(), text: '新任务' }]);

// 删除
setTodos(todos.filter(t => t.id !== id));

// 修改某个
setTodos(todos.map(t => t.id === id ? { ...t, done: !t.done } : t));

// 排序
setTodos([...todos].sort((a, b) => a.id - b.id));

⚠️ 坑 3:push/pop/splice 等会修改原数组的方法不能用,要用 concat/filter/map/slice 这类返回新数组的方法。

七、State 提升(共享状态) ​

兄弟组件共享 state,提升到最近共同父组件:

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

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

function DisplayA({ count }: { count: number }) {
  return <div>A 显示:{count}</div>;
}

function DisplayB({ count }: { count: number }) {
  return <div>B 显示:{count}</div>;
}

八、本章小结 ​

要点关键
useState返回 [值, setter]
不可变更新对象/数组要新建,不能直接改
函数式更新setX(prev => ...) 避免异步陷阱
数组操作用 map/filter/slice,不用 push/splice
对象操作{...obj, key: value}
State 提升兄弟共享 state 提到共同父组件

动手练习 ​

  1. 计数器:做一个 ±N 按钮,传入 step 控制步长(用函数式更新)
  2. Todo 列表:增、删、改、勾选完成,所有操作通过 setter
  3. 表单对象:维护 {username, password, remember},封装 update(key, value) 辅助函数
  4. 找坑:下面代码哪里错?
    tsx
    const [list, setList] = useState([1, 2, 3]);
    list.push(4);
    setList(list);

下一章:第 5 章:事件处理 →

本站基于 VitePress 构建 · 由 StackHub 团队维护