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 每次点击时都会运行一个函数,事件 章节接下来会详细讲,现在你先姑且相信就好。这个函数调用 setCount(count + 1),这个调用做两件事。它存储新值,然后重新渲染 Counter,这样屏幕上就显示更新后的数字了。调用 setter 是向 React 发出信号,说明有东西改变了,组件应该再运行一次。
根据前一个值进行更新
你经常会看到 setter 被调用时传的是一个函数而不是普通的值:
setCount(c => c + 1)这就是更新函数形式。React 用最新的值来调用你的函数,然后把你返回的东西作为下一个 state。每当新值是建立在旧值的基础上时就用它,特别是当你连续更新多次时。这两行代码看起来是一回事,但实际上不是:
setCount(count + 1)
setCount(count + 1)count 的两次读取都看到这次渲染中的同一个值,所以合起来计数只增加了一。更新函数形式就能解决这个问题,因为每次调用都会收到前一次调用的结果:
setCount(c => c + 1)
setCount(c => c + 1)现在计数增加了两次。反转布尔值的方式是一样的,这也是你最常见到更新函数形式的地方:setIsOpen(open => !open) 取当前值并返回它的反面。当你的新值取决于当前值时,更新函数形式是安全的默认选择。
State 是每个组件实例独立的
每次你渲染一个组件时,它都会有自己独立的 state。同一个页面上的两个计数器各自保存自己的计数:
function App() {
return (
<>
<Counter />
<Counter />
</>
)
}点击第一个按钮不会影响第二个。它们共享相同的 Counter 代码,但每一个都拥有独立的值。State 属于屏幕上的每个实例,所以一个函数可以驱动许多独立的副本。
替换 state,不要修改它
你通过给 React 一个新值来更新 state,React 会将新值和旧值进行比较,以判断什么改变了。直接修改现有的值会跳过这个信号,所以屏幕会显示过期的内容。这对对象和数组特别重要。要向数组添加东西,应该构建一个新数组而不是向旧数组中 push:
// 跳过更新:React 永远看不到新值
todos.push(newTodo)
setTodos(todos)
// 有效:一个全新的数组
setTodos(prev => [...prev, newTodo])对象也是一样的:把旧对象展开到一个新对象中,应用你的更改,然后从更新函数中返回它。把从 useState 获取的值视为 只读,始终给 setter 传递一个新的东西。
嵌套需要特别小心。展开一个对象只复制它的顶层,所以其中嵌套的对象仍然是同一个对象,修改那个嵌套对象会影响到原始对象:
// 修改了原始对象:嵌套的 address 对象仍然与新对象共享
user.address.city = '北京'
setUser({ ...user })
// 有效:每一层都有新对象,直到做出改变的地方
setUser(prev => ({
...prev,
address: { ...prev.address, city: '北京' }
}))对象周围的括号让箭头函数返回它,而不是把它当作代码块来读。每层直到做出改变的地方都需要自己的展开,这在嵌套加深时会变得不方便。当你发现自己在写一串这样的代码时,通常意味着 state 需要扁平化成更小的部分。
修改列表中的单个项
修改数组中已有的项是人们最常卡住的地方。工具是 map。遍历数组,对你要找的项返回一个修改后的副本,其他项保持原样返回:
function toggleTodo(id) {
setTodos(prev => prev.map(todo =>
todo.id === id ? { ...todo, done: !todo.done } : todo
))
}Setter 采用更新函数形式,因为下一个数组是从当前数组构建的。三元运算符做选择:匹配的 todo 得到一个 done 被反转的副本,其他东西直接通过。那些直接通过的项仍然是它们一直以来的同一个对象,这很好。只有你改变的那一个需要是新的,加上装着它们的数组,map 为 setter 构建了那个新数组。
删除项用 filter,添加项用前面章节的展开。这三种方法涵盖了大部分列表更新。
当初始值计算成本高时
JavaScript 在每次渲染时都会计算你传给 useState 的参数,在 useState 本身运行之前,React 只在第一次时保留结果。对于像 0 这样的值,成本几乎没有。对于一个做真正工作的函数调用,它会在每次渲染时运行,结果在第一次之后就被丢弃了:
// buildBoard() 在每次渲染时都会运行,它的结果在第一次之后被丢弃
const [board, setBoard] = useState(buildBoard())
// buildBoard 只会在第一次渲染时被调用一次
const [board, setBoard] = useState(buildBoard)第一行调用函数并把结果交给 React。第二行把函数本身交给 React,让 React 调用它一次。这种做法要留给真正成本高的工作,比如从存储中读取、生成大型结构或繁重计算。在 useState(0) 上这只是噪音。
版本说明
在 class 组件中,state 存储在 this.state 中,你用 this.setState 来更新它。useState 是函数组件的等价物,也是现代 React 中保存 state 的标准方式。
useState 会记住它。你拿到值和一个 setter,调用那个 setter 既存储新值也会重绘组件。 当新值建立在旧值基础上时,用 setCount(c => c + 1) 这样你总是在用最新的值。当那个值是数组或对象时,给 setter 一个全新的、应用了你改变的那个。
接下来:事件,你一直在写的那些 onClick 处理程序会得到一个恰当的讲解。

