Skip to content
第 146 / 250 章前端⏱ 10 分钟阅读

第 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 => ...)
对象/数组展开运算符返回新对象
提升多个组件共享时提升到共同父组件

动手练习

  1. 写一个 Todo 组件,能添加、删除、勾选
  2. 写一个表单,包含 name/email/age 字段
  3. 写一个分页组件,显示当前页和总页数
  4. 重构一个用 props 传状态的组件,用 useState

推荐阅读


下一章:第 147 章:条件渲染与列表

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