React公式サイトのドキュメントが2023年3月16日に改訂されました(「Introducing react.dev」参照)。本稿は、基本解説の「State as a Snapshot」をかいつまんでまとめた記事です。ただし、コードにはTypeScriptを加えました。反面、初心者向けのJavaScriptの基礎的な説明は省いています。
なお、本シリーズ解説の他の記事については「React + TypeScript: React公式ドキュメントの基本解説『Learn React』を学ぶ」をご参照ください。
状態変数は、標準JavaScriptの変数のように、値が読み書きできると思えるかもしれません。けれど、状態のふるまいは「スナップショット」(筆者注: そのときどきの情報の記録)に近いです。状態の設定はすでに定められた値の変更ではありません。再レンダーの起動です。
状態の設定によるレンダーの起動
Reactのユーザーインタフェースは、インタラクションなどのイベントに応じて直接更新されるのではありません。状態が改められると、Reactはコンポーネントを再レンダーします。逆に、イベントに応じたインタフェースを表示するためには、状態の更新が求められるのです。
たとえば、つぎのコード例ではフォーム(Form
)の[Send]ボタンをクリックすると、状態変数(isSent
)が更新され、ReactはUIを再レンダーします(サンプル001)。
import { ChangeEventHandler, FormEventHandler, useState } from 'react';
import { Form } from './Form';
import { SendMessage } from './SendMessage';
const sendMessage = (message: string) => {
// ...
};
export default function App() {
const [isSent, setIsSent] = useState(false);
const [message, setMessage] = useState<any>('Hi!');
const handleChange: ChangeEventHandler<HTMLTextAreaElement> = ({
target: { value }
}) => {
setMessage(value);
};
const handleSubmit: FormEventHandler<HTMLFormElement> = (event) => {
event.preventDefault();
setIsSent(true);
sendMessage(message);
};
return isSent ? (
<SendMessage />
) : (
<Form
handleChange={handleChange}
handleSubmit={handleSubmit}
message={message}
/>
);
}
import { ChangeEventHandler, FC, FormEventHandler } from 'react';
type Props = {
handleChange: ChangeEventHandler<HTMLTextAreaElement>;
handleSubmit: FormEventHandler<HTMLFormElement>;
message: string;
};
export const Form: FC<Props> = ({ handleChange, handleSubmit, message }) => {
return (
<form onSubmit={handleSubmit}>
<textarea placeholder="Message" value={message} onChange={handleChange} />
<button type="submit">Send</button>
</form>
);
};
サンプル001■React + TypeScript: State as a Snapshot 01
ボタンクリックで何が起こったのか、順を追って確かめましょう。
- まず
onSubmit
イベントハンドラが呼び出されます。 - そこで実行されるのが状態変数設定関数
setIsSent
です。新たな値(true
)はレンダリングのキューに加えられます。 - そしてReactが改められた状態変数値(
isSent
)にもとづいてコンポーネントを再レンダーするのです。
レンダリングはそのときとったスナップショット
「レンダリング」とは、Reactが関数であるコンポーネントを呼び出すことです。関数が返すJSXは、いわばそのときのUIのスナップショットといえるでしょう。プロパティやイベントハンドラ、ローカル変数は、すべてレンダリング時の状態にもとづいて計算されます。
返されるUIのスナップショットは、インタラクティブです。そのため、入力に応じて何を起こすか決めるイベントハンドラなどのロジックも含まれます。そこで、Reactはこのスナップショットに合わせて画面を更新したうえで、イベントハンドラとつなぎ込むのです。結果として、たとえばボタンを押せば、JSXのクリックハンドラが呼び出されます。
Reactによるコンポーネントの再レンダーの進め方はつぎのとおりです。
- 関数の実行: Reactが再び関数を呼び出します。
- スナップショットの計算: 関数が返すのは新たなJSXのスナップショットです。
- DOMツリーの更新: Reactは戻り値のスナップショットに合わせて画面を更新します。
コンポーネントにとってのメモリの役割となる状態は、関数が処理を終えたら消滅する通常の変数とは別です。状態は実際には関数の外で、React自体に保持されます。Reactがコンポーネントを呼び出すと、得られるのはそのレンダーに用いるべき状態のスナップショットです。そして、コンポーネントが返すUIのスナップショットには、最新のプロパティやイベントハンドラがJSXに含まれています。それらはすべて、そのレンダーの状態値にもとづいて計算されるのです。
状態のスナップショットを理解していただくため、つぎのコードの結果を考えてみましょう。ボタン(<button>
)クリックのイベントハンドラ(onClick
)で、カウンタの状態変数(number
)の設定関数(setNumber
)が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);
alert(number);
}}
>
+3
</button>
</>
);
}
はじめてボタンクリックした結果を見ると、表示されたカウンタの状態変数値(number
)は1です。つまり、初期値0から1しか加算されていません。そして、警告ダイアログには、状態変数の値として0が示されます。
レンダリング時に設定された状態変数は、あくまでつぎのレンダーで使われる値です。そのときのレンダリングは直近の状態変数値(0)にもとづきます。ですから、何度状態変数(number
)を設定関数(setNumber
)で加算しようと、直近の値のまま変わらないのです。
時間の経過と状態
前掲のコード例で、状態変数が初期値(0)のままだったのは、警告ダイアログをすぐに開いたからではないかと感じるかもしれません。そこで、つぎのように書き替えて、少し遅らせて確かめてみましょう。
export default function Counter() {
return (
<>
<h1>{number}</h1>
<button
onClick={() => {
setNumber(number + 1);
setNumber(number + 1);
setNumber(number + 1);
// alert(number);
setTimeout(() => {
alert(number);
}, 3000);
}}
>
+3
</button>
</>
);
}
それでも、表示される値は初期値のまま変わりません。Reactがコンポーネントの呼び出しによりUIのスナップショットを得たら、状態の値は「固定」されます。たとえ、コードを非同期にしても、状態変数値はそのレンダー中は変わらないのです。
では、状態変数の設定を遅らせるとどうでしょうか。つぎのコード例で確かめます(サンプル002)。
import { useState } from 'react';
import './styles.css';
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button
onClick={() => {
setNumber(number + 1);
setNumber(number + 1);
setNumber(number + 1);
setTimeout(() => {
setNumber(number + 10);
}, 1000);
setTimeout(() => {
alert(number);
}, 3000);
}}
>
+3
</button>
</>
);
}
サンプル002■React + TypeScript: State as a Snapshot 02
画面上は、少し遅れてカウンターの数値が(1から10に)変わります。けれど、警告ダイアログに表示されるのはやはり初期値(0)です。それに、時間をおいて設定されたカウンターの値(10)も、状態変数値(number
)として参照されたのは初期値であることがおわかりいただけるでしょう。
Reactはひとつのレンダーのイベントハンドラの中では状態変数値を「固定」しておきます。それは、コードの実行中に状態が変わる心配はないということです。
まとめ
この記事では、つぎのような項目についてご説明しました。
- 状態を設定すると新たなレンダーが求められます。
- Reactが状態を保持するのは関数の外です。
-
useState
を呼び出すと、Reactからそのレンダーにおける状態のスナップショットが得られます。 - 変数やイベントハンドラは再レンダー間で同じではありません。レンダーごとに備わるのはそれぞれ独自のイベントハンドラです。
- 各レンダー(およびその中の関数)はつねに、Reactがそのレンダーに与えた状態のスナップショットを参照します。
- イベントハンドラが参照するのはそれがつくられたレンダーの状態変数値です。