状态变量看起来可能像你可以读取和写入的普通 JavaScript 变量。但是,状态更像是一个快照。设置状态不会改变你已有的状态变量,而是会触发重新渲染。
你将学习
- 如何设置状态来触发重新渲染
- 状态何时以及如何更新
- 为什么状态在你设置后不会立即更新
- 事件处理程序如何访问状态的“快照”
设置状态触发渲染
你可能会认为你的用户界面会直接响应用户事件(例如点击)而发生变化。在 React 中,它的工作方式与这种思维模型略有不同。在上一页,你看到设置状态会请求 React 重新渲染。这意味着为了让界面对事件做出反应,你需要_更新状态_。
在这个例子中,当你按下“发送”按钮时,setIsSent(true)
告诉 React 重新渲染 UI
import { useState } from 'react'; export default function Form() { const [isSent, setIsSent] = useState(false); const [message, setMessage] = useState('Hi!'); if (isSent) { return <h1>Your message is on its way!</h1> } return ( <form onSubmit={(e) => { e.preventDefault(); setIsSent(true); sendMessage(message); }}> <textarea placeholder="Message" value={message} onChange={e => setMessage(e.target.value)} /> <button type="submit">Send</button> </form> ); } function sendMessage(message) { // ... }
以下是单击按钮时发生的情况
onSubmit
事件处理程序执行。setIsSent(true)
将isSent
设置为true
并排队新的渲染。- React 根据新的
isSent
值重新渲染组件。
让我们仔细看看状态和渲染之间的关系。
渲染会获取时间快照
“渲染”意味着 React 正在调用你的组件(一个函数)。从该函数返回的 JSX 就像 UI 的时间快照。它的 props、事件处理程序和局部变量都是使用它在渲染时的状态计算的。
与照片或电影帧不同,你返回的 UI“快照”是交互式的。它包括像事件处理程序这样的逻辑,这些逻辑指定响应输入时会发生什么。React 更新屏幕以匹配此快照并连接事件处理程序。因此,按下按钮将触发你 JSX 中的点击处理程序。
当 React 重新渲染组件时
- React 再次调用你的函数。
- 你的函数返回一个新的 JSX 快照。
- 然后,React 更新屏幕以匹配你的函数返回的快照。
作为组件的内存,状态不像一个在函数返回后消失的普通变量。状态实际上“存在”于 React 本身——就像在架子上一样!——在你的函数之外。当 React 调用你的组件时,它会为你提供该特定渲染的状态快照。你的组件返回 UI 的快照,其 JSX 中包含一组新的 props 和事件处理程序,所有这些都是使用该渲染的状态值计算的!
这是一个小实验,向你展示它是如何工作的。在这个例子中,你可能期望点击“+3”按钮会将计数器递增三次,因为它三次调用setNumber(number + 1)
。
看看点击“+3”按钮时会发生什么
import { useState } from 'react'; export default function Counter() { const [number, setNumber] = useState(0); return ( <> <h1>{number}</h1> <button onClick={() => { setNumber(number + 1); setNumber(number + 1); setNumber(number + 1); }}>+3</button> </> ) }
请注意,number
每次点击只递增一次!
设置状态只会更改_下一个_渲染的状态。在第一次渲染期间,number
为0
。这就是为什么在_该渲染的_ onClick
处理程序中,即使在调用setNumber(number + 1)
之后,number
的值仍然是0
<button onClick={() => {
setNumber(number + 1);
setNumber(number + 1);
setNumber(number + 1);
}}>+3</button>
以下是此按钮的点击处理程序告诉 React 要执行的操作
setNumber(number + 1)
:number
为0
,所以为setNumber(0 + 1)
。- React 准备在下一次渲染中将
number
更改为1
。
- React 准备在下一次渲染中将
setNumber(number + 1)
:number
为0
,所以为setNumber(0 + 1)
。- React 准备在下一次渲染中将
number
更改为1
。
- React 准备在下一次渲染中将
setNumber(number + 1)
:number
为0
,所以为setNumber(0 + 1)
。- React 准备在下一次渲染中将
number
更改为1
。
- React 准备在下一次渲染中将
即使您调用了三次 setNumber(number + 1)
,但在本次渲染的事件处理程序中,number
始终为 0
,因此您三次将状态设置为 1
。这就是为什么在您的事件处理程序完成后,React 使用 number
等于 1
而不是 3
来重新渲染组件的原因。
您也可以通过在代码中用其值替换状态变量来形象化地理解这一点。由于本次渲染中 number
状态变量为 0
,因此其事件处理程序如下所示
<button onClick={() => {
setNumber(0 + 1);
setNumber(0 + 1);
setNumber(0 + 1);
}}>+3</button>
对于下一次渲染,number
为 1
,因此那次渲染的点击处理程序如下所示
<button onClick={() => {
setNumber(1 + 1);
setNumber(1 + 1);
setNumber(1 + 1);
}}>+3</button>
这就是为什么再次点击按钮会将计数器设置为 2
,然后在下一次点击中设置为 3
,依此类推。
状态随时间变化
好吧,这很有趣。试试猜猜点击此按钮会发出什么警报
import { useState } from 'react'; export default function Counter() { const [number, setNumber] = useState(0); return ( <> <h1>{number}</h1> <button onClick={() => { setNumber(number + 5); alert(number); }}>+5</button> </> ) }
如果您使用之前的替换方法,您可以猜到警报显示“0”
setNumber(0 + 5);
alert(0);
但是,如果您在警报上设置一个计时器,使其仅在组件重新渲染之后触发呢?它会显示“0”还是“5”?猜猜看!
import { useState } from 'react'; export default function Counter() { const [number, setNumber] = useState(0); return ( <> <h1>{number}</h1> <button onClick={() => { setNumber(number + 5); setTimeout(() => { alert(number); }, 3000); }}>+5</button> </> ) }
惊讶吗?如果您使用替换方法,您可以看到传递给警报的状态的“快照”。
setNumber(0 + 5);
setTimeout(() => {
alert(0);
}, 3000);
当警报运行时,存储在 React 中的状态可能已经更改,但它是使用用户与之交互时状态的快照进行调度的!
状态变量的值在一次渲染中永远不会改变,即使其事件处理程序的代码是异步的。在该渲染的 onClick
中,即使在调用 setNumber(number + 5)
之后,number
的值仍然为 0
。当 React 通过调用您的组件“拍摄 UI 快照”时,其值被“固定”。
这是一个示例,说明这如何使您的事件处理程序不易出现计时错误。下面是一个带有五秒延迟的消息发送表单。想象一下这种情况
- 您按下“发送”按钮,将“你好”发送给爱丽丝。
- 在五秒延迟结束之前,您将“收件人”字段的值更改为“鲍勃”。
您期望alert
显示什么?它会显示“你对爱丽丝说了你好”吗?还是会显示“你对鲍勃说了你好”?根据您的了解进行猜测,然后尝试一下
import { useState } from 'react'; export default function Form() { const [to, setTo] = useState('Alice'); const [message, setMessage] = useState('Hello'); function handleSubmit(e) { e.preventDefault(); setTimeout(() => { alert(`You said ${message} to ${to}`); }, 5000); } return ( <form onSubmit={handleSubmit}> <label> To:{' '} <select value={to} onChange={e => setTo(e.target.value)}> <option value="Alice">Alice</option> <option value="Bob">Bob</option> </select> </label> <textarea placeholder="Message" value={message} onChange={e => setMessage(e.target.value)} /> <button type="submit">Send</button> </form> ); }
React 保持状态值在一个渲染的事件处理程序中“固定”。您无需担心代码运行期间状态是否已更改。
但是,如果您想在重新渲染之前读取最新的状态怎么办?您需要使用状态更新函数,这将在下一页介绍!
总结
- 设置状态请求新的渲染。
- React 将状态存储在组件外部,就像在架子上一样。
- 当您调用
useState
时,React 会为您提供该渲染的状态快照。 - 变量和事件处理程序不会“延续”到重新渲染。每次渲染都有其自己的事件处理程序。
- 每次渲染(及其内部函数)始终会“看到”React 提供给该渲染的状态快照。
- 您可以在事件处理程序中在脑中替换状态,这与您思考渲染的 JSX 的方式类似。
- 过去创建的事件处理程序具有创建它们的渲染中的状态值。
挑战 1的 1: 实现交通信号灯
这是一个当按下按钮时切换的行人过街信号灯组件
import { useState } from 'react'; export default function TrafficLight() { const [walk, setWalk] = useState(true); function handleClick() { setWalk(!walk); } return ( <> <button onClick={handleClick}> Change to {walk ? 'Stop' : 'Walk'} </button> <h1 style={{ color: walk ? 'darkgreen' : 'darkred' }}> {walk ? 'Walk' : 'Stop'} </h1> </> ); }
在点击处理程序中添加一个 alert
。当灯是绿灯并显示“通行”时,点击按钮应显示“接下来是停止”。当灯是红灯并显示“停止”时,点击按钮应显示“接下来是通行”。
将 alert
放在 setWalk
调用之前还是之后有区别吗?