State
函数内的普通变量在函数执行完后就被遗忘了,所以无法保存随时间变化的值并显示在屏幕上。State 就是来填补这个空缺的:它让组件在多次渲染之间记住某个值。Props 是从外部传入的数据,组件永远不会改变它;而 state 是组件自己拥有并自己更新的数据。useState Hook 给了组件这种记忆能力,并给了你一个更新值的方式,这个方式还会告诉 React 重新绘制界面。
这是一个计数器的例子:
import { useState } from 'react'
function Counter() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount(count + 1)}>{count}</button>
}useState(0) 设置起始值,然后返回一对东西:当前的值 count 和改变它的函数 setCount。这两个名字由你决定;[value, setValue] 这种形式是约定俗成的。按钮的 onClick 每次点击时都会运行一个函数,Events 这一章会完整介绍它,现在先相信我就行。那个函数调用 setCount(count + 1),这个调用做了两件事。它存储新值,并重新渲染 Counter 让屏幕显示更新后的数字。调用 setter 就是告诉 React 有什么东西改变了,组件应该再运行一次。
基于上一个值来更新
你经常会看到 setter 被调用时传的是一个函数,而不是普通的值:
setCount(c => c + 1)这就是 updater 形式。React 会用最新的值来调用你的函数,然后用你返回的值作为下一个 state。只要新值是根据旧值计算出来的,就用这种形式,特别是当你连续更新多次时。这两行看起来像是要做的事,但实际上做不了:
setCount(count + 1)
setCount(count + 1)两次对 count 的读取在这次渲染中看到的是同一个值,所以合起来只会把 count 增加一。Updater 形式解决了这个问题,因为每次调用都会收到前一次的结果:
setCount(c => c + 1)
setCount(c => c + 1)现在 count 会增加二。翻转布尔值也是一样的道理,而且是你最常见到 updater 形式的地方:setIsOpen(open => !open) 获取当前值然后返回它的反面。当你的新值取决于当前值时,updater 形式就是安全的默认做法。
State 是每个组件实例独有的
每次你渲染一个组件,它都会拥有自己独立的 state。同一个页面上的两个计数器各自保持自己的计数:
function App() {
return (
<>
<Counter />
<Counter />
</>
)
}点击第一个按钮不会影响第二个。它们共享相同的 Counter 代码,但每一个都持有独立的值。State 属于屏幕上的每个实例,所以一个函数可以驱动许多独立的副本。
替换 state,不要修改它
你通过给 React 一个新值来更新 state,React 会比较新旧值来判断什么改变了。直接修改现有的值会跳过这个信号,所以屏幕会变得陈旧。这对对象和数组最重要。要往数组里添加东西,应该构建一个新数组而不是推入到旧数组:
// 跳过更新:React 永远看不到新值
todos.push(newTodo)
setTodos(todos)
// 有效:一个全新的数组
setTodos(prev => [...prev, newTodo])对象也是一样:把旧对象展开到一个新对象里,应用你的改变,然后从 updater 返回那个新对象。把你从 useState 得到的值当作 只读的,总是给 setter 传一个新东西。
嵌套需要特别注意。展开一个对象只复制它的顶层,所以嵌套在里面的对象仍然是同一个对象,修改那个嵌套的对象会影响到原对象:
// 修改了原对象:嵌套的 address 对象在新旧之间仍然是共享的
user.address.city = 'Beijing'
setUser({ ...user })
// 有效:从顶层到改变处的每一层都是新对象
setUser(prev => ({
...prev,
address: { ...prev.address, city: 'Beijing' }
}))对象外面的圆括号是让箭头函数返回它,而不是把它读作一段代码。从顶层到改变处的每一层都需要自己的展开,这在嵌套变深时会变得很冗长。当你发现自己在写一串展开时,那通常意味着 state 想要被拆散成更小的片段。
修改列表中的一项
修改数组里已经存在的某一项是人们最容易卡住的地方。处理这个的工具是 map。遍历数组,对你要找的项返回一个改变后的副本,其他每一项原样返回:
function toggleTodo(id) {
setTodos(prev => prev.map(todo =>
todo.id === id ? { ...todo, done: !todo.done } : todo
))
}Setter 采用 updater 形式,因为下一个数组是根据当前的数组构建的。三元运算符做了选择:匹配的 todo 得到一个副本,其中 done 被翻转,其他一切原样通过。那些通过的项是它们一直以来都是的同一个对象,这是可以的。只有你改变的那一项需要是新的,连同包含它的数组,而 map 为 setter 构建了那个新数组。
删除一项用 filter,添加一项用上一部分的展开。这三个方法合起来覆盖了大部分列表更新。
当初始值很昂贵时
JavaScript 在 useState 运行之前先计算你传给它的参数,React 只在第一次时保存结果。对于 0 这样的值,花费不了多少。对于做真实工作的函数调用,它会在每次渲染时都运行,而结果在第一次之后就被丢弃:
// buildBoard() 在每次渲染时都运行,结果在第一次之后被丢弃
const [board, setBoard] = useState(buildBoard())
// buildBoard 只在第一次渲染时被调用一次
const [board, setBoard] = useState(buildBoard)第一行调用函数并把结果交给 React。第二行把函数本身交给 React,让 React 调用它一次。把这个留给真正花钱的工作,比如从存储读取,生成大结构,或繁重的计算。在 useState(0) 上用这个就是噪音。
版本说明
在类组件里,state 住在 this.state 里,你用 this.setState 更新它。useState 是函数组件的等价物,是现代 React 中保存 state 的标准方式。
useState 记住了。你拿回值和 setter,调用那个 setter 既存储新值也重绘组件。当新值是根据旧值构建时,用 setCount(c => c + 1) 这样你总是基于最新的在工作。当那个值是数组或对象时,给 setter 一个全新的,应用了你的改变。 下一步:Events,在那里你一直在写的那些 onClick 处理器会得到好好的解释。

