Archive Note
React Hook 学习 1:useState 的状态模型
useState 是 React Hook 学习的起点,但它最容易被误解的地方,不是 API 本身,而是“状态到底是什么”。
很多初学者会把它理解成“一个可以变的变量”,但在真实开发里,更准确的理解应该是:useState 为当前组件实例提供了一份跨渲染保存的数据快照,而更新状态会触发 React 重新执行组件函数,用新的状态值生成下一次 UI。
一、原理:状态不是变量,而是渲染输入
先看一个常见写法:
const [count, setCount] = useState(0);
这里的 count 看起来像一个普通变量,但它和 let count = 0 的本质完全不同。
- 普通变量:函数每次重新执行都会重置
- state:React 会在组件实例上保存这份值,并在下一次渲染时重新提供给你
所以你可以把组件理解成:
UI = f(state, props)
useState 的职责,就是让这份 state 在多次渲染之间稳定存在。
二、开发场景:什么时候应该用 useState
适合用 useState 的场景通常有一个共同点:这个值会影响渲染结果,而且它会在用户交互后发生变化。
比如:
- 表单输入框内容
- 弹窗开关状态
- 当前选中的 tab
- 请求完成后的列表数据
- 加载态、错误态
例如一个标签切换:
const [activeTab, setActiveTab] = useState("overview");
这里 activeTab 决定当前显示哪个区域,所以它就是标准的 state。
但如果一个值只是为了函数内部临时计算,并不会影响渲染,就不应该塞进 state。
三、最常见误区:把 state 当成“立刻更新”的变量
很多人第一次写 React 时都会踩这个坑:
setCount(count + 1);
console.log(count);
然后发现打印出来的还是旧值,于是觉得“React 是异步的”。
更准确的说法是:当前这次函数执行拿到的是旧渲染快照,setCount 触发的是下一次渲染。
也就是说,不是 count 会在当前这行代码执行后立刻变掉,而是 React 会安排一次新的渲染,在下一轮里把新值给你。
四、闭包问题:为什么连续更新会出错
下面这段代码经常让人困惑:
setCount(count + 1);
setCount(count + 1);
直觉上看它应该加 2,但实际通常只加 1。原因是这两次更新都读取了同一个旧的 count。
正确写法是函数式更新:
setCount((prev) => prev + 1);
setCount((prev) => prev + 1);
这里 React 会把前一次更新结果作为下一次的 prev,因此最终才会得到正确结果。
经验法则:
- 新状态依赖旧状态时,用函数式更新
- 新状态不依赖旧状态时,可以直接传值
五、状态怎么拆:合并还是分开
这是实际开发里很重要的问题。
更适合拆开的情况
const [keyword, setKeyword] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
这些状态虽然都属于同一个页面,但职责不同、变化频率不同,拆开更清楚。
更适合合并的情况
const [form, setForm] = useState({
name: "",
email: ""
});
当几个字段天然属于一个整体对象时,合并更符合数据结构。
判断标准
不要问“写法哪个更短”,要问:
- 这些值是不是一个业务实体
- 它们是否总是一起变化
- 拆开后会不会更容易维护
六、不要把派生数据也放进 state
这是另一个高频错误。
比如:
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
const [fullName, setFullName] = useState("");
这里 fullName 可以由前两个值直接推导出来,就不应该再单独存 state。否则你会制造同步问题。
更好的写法:
const fullName = `${firstName} ${lastName}`.trim();
经验法则:
- 能从已有 state / props 计算出来的值,优先直接算
- 只有真正独立、会被更新、会影响渲染的值,才存 state
七、真实开发建议
1. 初始化值要体现真实业务含义
不要为了省事把所有东西都初始化成空字符串或空对象。
例如:
- 未加载的数据:
null - 空列表:
[] - 未选择项:
null - 开关:
false
初始化值本身就是业务语义的一部分。
2. 避免 state 过深
如果你开始写出这种代码:
setState((prev) => ({
...prev,
section: {
...prev.section,
panel: {
...prev.section.panel,
visible: true
}
}
}));
说明状态结构已经开始失控。此时应该考虑:
- 拆状态
- 抽 reducer
- 重新设计数据形状
3. 把 state 看成“渲染驱动器”
问自己一个问题:
这个值变了以后,UI 需要重新画吗?
如果答案是“需要”,它可能是 state。 如果答案是“不需要”,它大概率不该是 state。
八、这一篇最该记住的结论
useState管的是会驱动 UI 重渲染的数据- state 是跨渲染保存的值,不是普通变量
- 更新 state 触发的是下一次渲染,不是当前值原地变化
- 依赖旧值更新时,优先用函数式更新
- 派生数据不要重复存 state
如果你能真正理解这些,后面学习 useEffect 和 useRef 时会顺很多,因为它们本质上都是在处理“渲染”和“非渲染”的边界。