スナップショットとしての状態

状態変数は、読み書きできる通常の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)isSenttrueに設定し、新しいレンダリングをキューに入れます。
  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がコンポーネントを呼び出すと、その特定のレンダリングの状態のスナップショットが提供されます。コンポーネントは、propsとイベントハンドラーの新しいセットを備えたUIのスナップショットをJSXで返し、それらはすべてそのレンダリングの状態値を使用して計算されます!

  1. Reactに状態の更新を指示する
  2. Reactが状態値を更新する
  3. Reactが状態値のスナップショットをコンポーネントに渡す

イラスト Rachel Lee Nabors

これがどのように機能するかを示す小さな実験をしてみましょう。この例では、「+3」ボタンをクリックすると、setNumber(number + 1)を3回呼び出すため、カウンターが3回インクリメントされると予想されるかもしれません。

「+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はクリックごとに1回しかインクリメントされません!

状態の設定は、次のレンダリングに対してのみ変更されます。最初のレンダリングでは、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 は次のレンダリングで number1 に変更する準備をします。
  2. setNumber(number + 1): number0 なので、setNumber(0 + 1) となります。
    • React は次のレンダリングで number1 に変更する準備をします。
  3. setNumber(number + 1): number0 なので、setNumber(0 + 1) となります。
    • React は次のレンダリングで number1 に変更する準備をします。

setNumber(number + 1) を3回呼び出したとしても、このレンダリングのイベントハンドラ内では number は常に 0 なので、状態を 1 に3回設定します。そのため、イベントハンドラの処理が終了した後、React は number3 ではなく 1 としてコンポーネントを再レンダリングします。

コード内で状態変数をその値で置き換えることで、これを視覚化することもできます。このレンダリングでは状態変数 number0 なので、そのイベントハンドラは次のようになります。

<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内では、setNumber(number + 5)が呼び出された後でも、numberの値は0のままです。その値は、Reactがコンポーネントを呼び出すことでUIのスナップショットを「取得した」ときに「固定」されました。

それがどのようにイベントハンドラをタイミングミスから保護するかを示す例を以下に示します。以下は、5秒間の遅延でメッセージを送信するフォームです。次のシナリオを考えてみましょう。

  1. 「送信」ボタンを押して、「こんにちは」をAliceに送信します。
  2. 5秒間の遅延が終了する前に、「宛先」フィールドの値を「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は、1つのレンダリングのイベントハンドラ内で状態値を「固定」します。コードの実行中に状態が変更されたかどうかを心配する必要はありません。

しかし、再レンダリングの前に最新の状態で読み込みたい場合は、次のページで説明する状態更新関数を使用する必要があります!

まとめ

  • 状態の設定は新しいレンダリングを要求します。
  • 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を追加します。信号が緑色で「歩行」と表示されている場合、ボタンをクリックすると「停止は次に来ます」と表示する必要があります。信号が赤色で「停止」と表示されている場合、ボタンをクリックすると「歩行は次に来ます」と表示する必要があります。

alertsetWalk呼び出しの前後に配置する場合、違いはありますか?