useState 是让组件在多次渲染之间记住某样东西的 Hook:一个计数、输入框里的文字、菜单是否展开。每次调用它的 setter,React 都会用新值重新渲染组件。
点击按钮,然后在编辑器中把 useState(0) 改成 useState(10)。预览会重新构建,计数从 10 开始。
语法
const [value, setValue] = useState(initialValue);
initialValue只在第一次渲染时使用。之后的每次渲染,React 都会忽略它,返回自己保存的值。value是本次渲染的当前值。setValue替换这个值,并安排一次新的渲染。
名字由你自己定。useState 返回一个包含两项的数组,方括号把它解构出来,所以 [isOpen, setIsOpen] 和 [email, setEmail] 是同一种写法。
在组件的顶层调用 useState,永远不要在循环、条件语句或嵌套函数中调用。React 按调用顺序把每次调用与它保存的值对应起来,所以每次渲染时这个顺序都必须相同(见 Hook 的规则)。
多个 state
一个组件可以按需多次调用 useState。每次调用都是一个独立的值。
state 是一张快照
setter 不会改变你手上的变量。它告诉 React 下一次渲染时这个值应该是什么。在那次渲染发生之前,当前这次渲染看到的一直是旧值。
打开预览下方的控制台,点击一次。处理函数输出的是旧值 0,接着组件输出一次新的渲染,值为 1。这没有任何问题:count 在整次渲染中都是一个常量,下一次渲染会得到一个新的。
更新函数
由于快照的存在,用 count + 1 调用三次 setter,结果只加了一,而不是三:每次调用计算的都是 0 + 1。改为传入一个函数,React 就会把上一次调用的结果传给下一次调用。
只要下一个值依赖上一个值,而且这次更新可能不是直接基于最新一次渲染执行的,就用更新函数的写法:在一个处理函数中多次设置、在 setInterval 回调中,或者在 Effect 中。
state 中的对象和数组
React 通过 Object.is 比较新旧值来决定是否重新渲染。如果你直接修改一个对象,再把同一个对象传回去,比较结果就是什么都没变,屏幕也就保持原样。永远要传入一个新的对象或数组。
{ ...user, age: user.age + 1 } 复制所有字段并覆盖其中一个。[...tags, 'code'] 添加一项,filter 删除一项,两者都返回一个新数组。更新数组和对象一页讲解了嵌套对象、编辑列表中的某一项,以及在中间插入元素。
惰性初始 state
传给 useState 的参数在每次渲染时都会被求值,尽管 React 只在第一次使用它。当计算它的开销很大时(读取 localStorage、解析一个很大的字符串),就传入一个函数。React 只在第一次渲染时调用它一次。
// Runs readSavedNotes() on every render, then throws the result away
const [notes, setNotes] = useState(readSavedNotes());
// Runs it once
const [notes, setNotes] = useState(readSavedNotes);
const [notes, setNotes] = useState(() => readSavedNotes());
用 key 重置 state
state 属于组件在树中的位置。如果你在同一个位置渲染一个不同的 key,React 会把它当作一个新组件,它的 state 也会从头开始。当表单编辑的对象发生变化时,这是重置表单最干净的方法。
输入一条消息,然后切换收件人:草稿被清空了。删掉 key={to} 再试一次,文字会保留下来,因为现在 React 看到的是同一个位置上的同一个 Draft。
常见错误
设置 state 之后立刻打印它。 你会看到旧值,如上所示。在组件函数体中打印,或者先把下一个值算出来存进变量,再使用这个变量。
直接修改 state。 user.age++; setUser(user) 传入的是同一个对象,所以什么都不会渲染。先复制一份。
在渲染期间调用 setter。 <button onClick={setCount(count + 1)}> 会在渲染时调用 setCount,这又会触发渲染,渲染又会再次调用它。React 会用 "Too many re-renders" 终止这个循环。请传入一个函数:onClick={() => setCount(count + 1)}。
存储可以计算出来的值。 如果 fullName 总是等于 first + ' ' + last,就在渲染时计算它,而不是再维护第三个可能不同步的 state。
常见问题
useState 返回什么?
一个正好有两项的数组:当前的值,以及一个替换它的函数。你用数组解构自己给它们命名,按照惯例写成 [thing, setThing]。
为什么我的 state 没有立即更新?
调用 setter 并不会改变你手上已有的那个变量。它是请求 React 重新渲染组件,新值会在下一次渲染中出现。在当前的事件处理函数中,作用域里仍然是旧值,所以紧跟在 setCount(count + 1) 之后的 console.log(count) 打印的是旧数字。
什么时候应该给 setter 传一个函数?
当新值依赖旧值,并且你要多次设置它,或者在定时器、Effect 中设置它时。setCount(c => c + 1) 总是从最新的值开始计算,而 setCount(count + 1) 是从本次渲染看到的值开始计算。
useState 可以保存对象或数组吗?
可以,但要把它当作只读的。构建一个新的对象或数组,再把它传给 setter(setUser({ ...user, name })、setItems([...items, item]))。直接修改现有的对象或数组不会触发渲染。
useState 和 useReducer 有什么区别?
两者都用来存储 state。useState 适合几个相互独立的值。useReducer 把更新逻辑移到一个接收 action 的函数中,当许多事件以相互关联的方式修改同一份 state 时,这样写更易读。