← Back to desk

Archive Note

React Hook 学习 1:useState 的状态模型

Filed As react-hook-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

如果你能真正理解这些,后面学习 useEffectuseRef 时会顺很多,因为它们本质上都是在处理“渲染”和“非渲染”的边界。