状态变量可能看起来像可以读取和写入的常规 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) {
  // ...
}

以下是当你点击按钮时会发生的情况

  1. 会执行 onSubmit 事件处理程序。
  2. setIsSent(true) 会将 isSent 设置为 true 并将新的渲染排入队列。
  3. React 会根据新的 isSent 值重新渲染组件。

让我们仔细看看状态和渲染之间的关系。

渲染会捕捉某个时间点的快照

“渲染” 意味着 React 正在调用你的组件,它是一个函数。你从该函数返回的 JSX 就像某个时间点 UI 的快照。它的 props、事件处理程序和局部变量都是 使用渲染时的状态 计算出来的。

与照片或电影帧不同,你返回的 UI“快照”是交互式的。它包含逻辑,例如指定响应输入时会发生什么的事件处理程序。React 会更新屏幕以匹配此快照并连接事件处理程序。因此,按下按钮将触发 JSX 中的点击处理程序。

当 React 重新渲染组件时

  1. React 会再次调用你的函数。
  2. 你的函数会返回一个新的 JSX 快照。
  3. 然后,React 会更新屏幕以匹配你的函数返回的快照。
  1. React 执行函数
  2. 计算快照
  3. 更新 DOM 树

插图作者 Rachel Lee Nabors

作为组件的内存,状态不像函数返回后就会消失的常规变量。状态实际上“存在”于 React 本身——就像在架子上!——在你的函数之外。当 React 调用你的组件时,它会为你提供该特定渲染的状态快照。你的组件会在其 JSX 中返回一个 UI 快照,其中包含一组新的 props 和事件处理程序,所有这些都是 使用该渲染中的状态值 计算出来的!

  1. 你告诉 React 更新状态
  2. React 更新状态值
  3. React 将状态值的快照传递给组件

插图作者 Rachel Lee Nabors

这里有一个小实验可以向你展示它是如何工作的。在这个例子中,你可能期望点击“+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 每次点击只递增一次!

设置状态只会在下一次渲染时更改它。在第一次渲染期间,number0。这就是为什么在该渲染的 onClick 处理程序中,即使在调用了 setNumber(number + 1) 之后,number 的值仍然为 0

<button onClick={() => {
setNumber(number + 1);
setNumber(number + 1);
setNumber(number + 1);
}}>+3</button>

以下是此按钮的点击处理程序告诉 React 要做的事情

  1. setNumber(number + 1)number0,所以 setNumber(0 + 1)
    • React 准备在下一次渲染时将 number 更改为 1
  2. setNumber(number + 1)number0,所以 setNumber(0 + 1)
    • React 准备在下一次渲染时将 number 更改为 1
  3. setNumber(number + 1)number0,所以 setNumber(0 + 1)
    • React 准备在下一次渲染时将 number 更改为 1

即使你在*此渲染*的事件处理程序中三次调用了 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>

对于下一次渲染,number1,因此*该渲染*的点击处理程序如下所示

<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 内部,number 的值在调用 setNumber(number + 5) 之后仍然是 0。当 React 通过调用你的组件 “获取 UI 的快照” 时,它的值就被 “固定” 了。

下面是一个例子,说明这如何使你的事件处理程序更不容易出现计时错误。下面是一个以五秒钟的延迟发送消息的表单。想象一下这种情况

  1. 你按下 “发送” 按钮,向 Alice 发送 “你好”。
  2. 在五秒钟的延迟结束之前,你将 “收件人” 字段的值更改为 “Bob”。

你希望 alert 显示什么?它会显示 “你对 Alice 说你好”?还是会显示 “你对 Bob 说你好”?根据你所知道的猜一猜,然后试一试

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 调用之前还是之后有区别吗?