狀態作為快照

狀態變數看起來像是可以讀取和寫入的普通 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 在時間點的快照。它的屬性、事件處理器和區域變數都是**使用其在渲染時的狀態**計算出來的。

與照片或電影畫面不同,你返回的 UI「快照」是互動式的。它包含邏輯,例如指定響應輸入時會發生什麼的事件處理器。React 會更新螢幕以符合此快照並連接事件處理器。因此,按下按鈕將觸發 JSX 中的點擊處理器。

當 React 重新渲染元件時,

  1. React 會再次呼叫你的函式。
  2. 你的函式會返回一個新的 JSX 快照。
  3. React 接著會更新螢幕以符合你的函式返回的快照
  1. React 執行函式
  2. 計算快照
  3. 更新 DOM 樹

插圖作者 Rachel Lee Nabors

作為元件的記憶體,狀態不像函式返回後就會消失的普通變數。狀態實際上「存在」於 React 本身——就像在架子上一樣!——在你的函式之外。當 React 呼叫你的元件時,它會為該特定渲染提供狀態的快照。你的元件會在 JSX 中返回 UI 的快照,其中包含一組全新的屬性和事件處理器,所有這些都是**使用該渲染中的狀態值**計算出來的!

  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 呼叫之前或之後是否有區別?