State 与 useState
Props 是"外部传进来"的数据,组件自己不能改;State 是"组件内部自己管"的数据,一变组件自动重新渲染。State 是 React 让界面"动起来"的核心机制,而 useState 是你接触的第一个 Hook。
1. 最简单的计数器
先看一个经典例子,理解 useState 的基本用法:
import { useState } from "react";
function Counter() {
// useState 返回一个数组:[当前值, 修改函数]
// 用解构取出,初始值是 0
const [count, setCount] = useState(0);
return (
<div>
<p>当前:{count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
<button onClick={() => setCount(count - 1)}>-1</button>
<button onClick={() => setCount(0)}>重置</button>
</div>
);
}每点一次 +1 按钮,发生的事是:调用 setCount(count + 1) → React 收到新值 → 重新渲染 Counter 组件 → 页面上的数字更新。这就是 React 的核心循环:state 变化 → 触发重渲染 → UI 自动更新。
2. useState 的基本规则
// useState 返回的解构名字可以随便起,但约定 set + 首字母大写
const [name, setName] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [user, setUser] = useState(null);
// ❌ 错误:setCount 不是 count++,而是"传新值"
const [count, setCount] = useState(0);
count++; // 不行,直接改 state 不会触发重渲染
setCount(count + 1); // 正确:用 set 函数传新值
// 初始值是"昂贵计算"时,用函数形式(只跑一次)
const [data, setData] = useState(() => loadFromStorage());
// 而不是 useState(loadFromStorage()),后者每次渲染都跑(慢)几个要点:useState(初始值) 返回一个二元数组,用解构 const [值, set函数] = useState(初始值) 取出。set 函数的作用是"告诉 React 这个 state 变了,请用新值重渲染"。不调用 set,组件永远不会更新。
3. 不可变更新(最重要的概念)
这是 React 新手最容易踩的坑,务必吃透:state 是不可变的,你必须给 set 函数传一个全新的对象/数组,而不是直接改原来的。
// ❌ 错误:直接改对象字段,React 检测不到变化
const [user, setUser] = useState({ name: "小明", age: 20 });
user.age = 21; // 直接改,React 不会重渲染!
setUser(user); // 引用没变,React 认为没变化
// ✅ 正确:用展开运算符造一个新对象
setUser({ ...user, age: 21 }); // 新对象,引用变了,触发更新
// ❌ 错误:数组直接 push
const [list, setList] = useState([1, 2, 3]);
list.push(4); // 直接改,不触发更新
// ✅ 正确:造一个新数组
setList([...list, 4]); // 末尾加
setList([0, ...list]); // 开头加
setList(list.filter(x => x !== 2)); // 删除元素
setList(list.map(x => x * 2)); // 修改元素为什么?因为 React 判断"state 是否变化"用的是引用比较(Object.is)。你直接改 user.age,user 这个变量指向的对象还是同一个(引用没变),React 就认为"没变化",不重渲染。造一个新对象 { ...user, age: 21 },引用变了,React 才会更新。
记住这个口诀:改 state 等于"换一个新值",不是"在原值上改"。数组用 ...、filter、map 这些"返回新数组"的方法;对象用 ... 展开。
4. 函数式更新:依赖上一次的值时
当新 state 依赖上一次的 state 时(比如 count + 1),直接用 count 可能踩"闭包陷阱"——因为 count 是本次渲染时的快照,连续多次调用 set 会用到旧值:
const [count, setCount] = useState(0);
// ❌ 依赖上一次的值时,直接用 count 有"闭包陷阱"
// 连续调用三次,setCount(count + 1) 实际只加了 1
const bad = () => {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
};
// ✅ 正确:用"函数式更新",React 把最新值传给你
const good = () => {
setCount(prev => prev + 1);
setCount(prev => prev + 1);
setCount(prev => prev + 1);
};
// 调用 good() 后,count 会变成 3规则:新 state 不依赖旧值,直接传值;依赖旧值,传函数 prev => ...。养成这个习惯,能避免一大类难调的 bug。
5. 多个 state vs 一个对象 state
一个组件可以有任意多个 state:
// 一个组件可以有多个 state
function SignupForm() {
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [agreed, setAgreed] = useState(false);
const [errors, setErrors] = useState([]);
// 当 state 互相关联时,可以合并成一个对象
// 但更新时要记得展开(不可变)
// const [form, setForm] = useState({ name: "", email: "" });
// setForm(prev => ({ ...prev, name: "新名字" }));
return (
<form>
<input value={name} onChange={e => setName(e.target.value)} />
<input value={email} onChange={e => setEmail(e.target.value)} />
</form>
);
}经验法则:互相独立的字段,用多个 state(简单清晰);强相关的字段(如表单的多个字段),合并成一个对象 state(更新时记得展开)。不要走极端——全用多个 state 会啰嗦,全塞一个对象会臃肿。
6. state 是"快照"
React 的 state 有个反直觉的特性:一次渲染里,count 是一个固定不变的快照。即使你调用了 setCount(count + 1),本次渲染里 count 还是旧值——新值要等下次渲染才生效。
- 这就是为什么"连续
setCount(count + 1)三次"只加 1:三次都用同一个旧count。 - 这也是为什么需要函数式更新:
prev => prev + 1让 React 把"最新值"传给你。
理解这一点,你就理解了 React 的"函数式"内核——它和传统的"变量赋值"思维完全不同。
7. state 要放对地方
不是所有数据都要放 state。判断原则:
- 会变、且需要驱动 UI 更新的数据 → 放 state。
- 从 props 传进来的 → 不要放 state,直接用 props(放 state 会导致两者不同步)。
- 不变的计算结果 → 用普通变量或
useMemo,不要放 state。
小结
State 让组件"活"起来。useState 返回值和 set 函数,改 state 必须传新值(不可变),依赖旧值时用函数式更新。掌握不可变更新,React 你就懂了一半。下一篇看用户操作怎么触发 state 变化——事件处理。
← 上一篇 Props 属性
下一篇 React 事件处理 →