State を使って入力に反応する
React は UI を操作するための宣言的な方法を提供します。UI の個々の部分を直接操作するのではなく、コンポーネントが取りうる異なる状態を記述し、ユーザーの入力に応じてそれらの状態を切り替えます。これは、デザイナーが UI について考える方法に似ています。
このページで学ぶこと
- 宣言的な UI プログラミングと命令的な UI プログラミングの違い
- コンポーネントが持つ様々な視覚状態を列挙する方法
- 異なる視覚状態間の変更をコードからトリガーする方法
宣言的な UI と命令的な UI の比較
UI の相互作用を設計する際、おそらくユーザーのアクションに応じて UI がどのように変化するかを考えることが多いでしょう。たとえば、ユーザーが回答を送信できるフォームを考えてみましょう。
- フォームに何かを入力すると、「送信」ボタンが有効になります。
- 「送信」ボタンを押すと、フォームとボタンが無効になり、スピナーが表示されます。
- ネットワークリクエストが成功した場合、フォームは非表示になり、「ありがとうございました」というメッセージが表示されます。
- ネットワークリクエストに失敗した場合、エラーメッセージが表示され、フォームが再び使用可能になります。
命令的プログラミングでは、上記はそのまま UI の相互作用の実装法に対応します。今起こったことに応じて UI を操作するための正確な命令を書かなければならないのです。例えば、車の中で隣に乗っている人に、曲がるたびに行き先を指示することを想像してみてください。

Illustrated by Rachel Lee Nabors
彼らはあなたがどこに行きたいか知りません、彼らはただあなたの指示に従うだけです。(そして、もし方向が間違っていたら、あなたは間違った場所に着いてしまいます!)これは 命令的 と呼ばれます。なぜなら、スピナーからボタンまでの各要素に対して、コンピューターに UI の 更新 を「指示」しなければならないからです。
この命令的 UI プログラミングの例では、フォームは React を使わずに作成されています。ブラウザの DOM を利用するだけです。
async function handleFormSubmit(e) { e.preventDefault(); disable(textarea); disable(button); show(loadingMessage); hide(errorMessage); try { await submitForm(textarea.value); show(successMessage); hide(form); } catch (err) { show(errorMessage); errorMessage.textContent = err.message; } finally { hide(loadingMessage); enable(textarea); enable(button); } } function handleTextareaChange() { if (textarea.value.length === 0) { disable(button); } else { enable(button); } } function hide(el) { el.style.display = 'none'; } function show(el) { el.style.display = ''; } function enable(el) { el.disabled = false; } function disable(el) { el.disabled = true; } function submitForm(answer) { // Pretend it's hitting the network. return new Promise((resolve, reject) => { setTimeout(() => { if (answer.toLowerCase() == 'istanbul') { resolve(); } else { reject(new Error('Good guess but a wrong answer. Try again!')); } }, 1500); }); } let form = document.getElementById('form'); let textarea = document.getElementById('textarea'); let button = document.getElementById('button'); let loadingMessage = document.getElementById('loading'); let errorMessage = document.getElementById('error'); let successMessage = document.getElementById('success'); form.onsubmit = handleFormSubmit; textarea.oninput = handleTextareaChange;
UI を命令的に操作することは、単一の例ではうまくいくかもしれませんが、より複雑なシステムでは指数関数的に難しくなります。例えばこのような様々なフォームでいっぱいのページを更新することを想像してみてください。新しい UI 要素や新しい相互作用を追加する場合、既存のすべてのコードを注意深くチェックして、バグ(例えば、何かを表示または非表示にすることを忘れていないか)を確認する必要があります。
React はこの問題を解決するために作られました。
React では、UI を直接操作することはありません。つまり、コンポーネントの有効化、無効化、表示、非表示を直接行うことはありません。代わりに、表示したいものを宣言することで、React が UI を更新する方法を考えてくれるのです。タクシーに乗ったとき、どこで曲がるかを正確に伝えるのではなく、どこに行きたいかを運転手に伝えることを思い浮かべてください。運転手はあなたをそこに連れて行くのが仕事ですし、あなたが考えもしなかった近道も知っているかもしれません!

Illustrated by Rachel Lee Nabors
UI を宣言的に考える
上記では、フォームを命令的に実装する方法を見てきました。React で考える方法をより理解するために、以下でこの UI を React で再実装する方法を確認していきます。
- コンポーネントの様々な視覚状態を特定する
- それらの状態変更を引き起こすトリガーを決定する
useState
を使用してメモリ上に状態を表現する- 必要不可欠でない状態変数をすべて削除する
- イベントハンドラーを接続して状態を設定する
Step 1: コンポーネントの様々な視覚状態を特定する
コンピュータサイエンスでは、 「ステートマシン」 がいくつかの「状態」のうちの1つであることを耳にすることがあります。デザイナーと一緒に仕事をしていると、さまざまな「視覚状態」のモックアップを見たことがあるかもしれません。React はデザインとコンピューターサイエンスの交点に位置しているため、これら両方のアイデアがインスピレーションの源になります。
まず、ユーザーが目にする可能性のある UI の様々な「状態」をすべて可視化する必要があります。
- 未入力:フォームには無効な「送信」ボタンがあります。
- 入力中:フォームには有効な「送信」ボタンがあります。
- 送信中:フォームは完全に無効化されます。スピナーが表示されます。
- 成功:フォームの代わりに「ありがとうございました」のメッセージが表示されます。
- エラー:入力中の状態と同じですが、追加のエラーメッセージがあります。
デザイナーのように、ロジックを追加する前に様々な状態の「モックアップ」を作成することをお勧めします。例えば、フォームの表示部分だけのモックを以下に示します。このモックはデフォルト値が 'empty'
の status
という props によって制御されます。
export default function Form({ status = 'empty' }) { if (status === 'success') { return <h1>That's right!</h1> } return ( <> <h2>City quiz</h2> <p> In which city is there a billboard that turns air into drinkable water? </p> <form> <textarea /> <br /> <button> Submit </button> </form> </> ) }
その props の名前は何でもよくて、命名は重要ではありません。status = 'empty'
を status = 'success'
に編集して、成功のメッセージが表示されるのを確認してみてください。モックアップを使えば、ロジックを結びつける前に、UI を素早く反復することができます。同じコンポーネントのより具体的なプロトタイプを以下に示しますが、これも status
プロパティによって「制御」されています。
export default function Form({ // Try 'submitting', 'error', 'success': status = 'empty' }) { if (status === 'success') { return <h1>That's right!</h1> } return ( <> <h2>City quiz</h2> <p> In which city is there a billboard that turns air into drinkable water? </p> <form> <textarea disabled={ status === 'submitting' } /> <br /> <button disabled={ status === 'empty' || status === 'submitting' }> Submit </button> {status === 'error' && <p className="Error"> Good guess but a wrong answer. Try again! </p> } </form> </> ); }
さらに深く知る
コンポーネントが多くの視覚状態を持つ場合、それらをすべて 1 つのページに表示することが便利な場合があります。
import Form from './Form.js'; let statuses = [ 'empty', 'typing', 'submitting', 'success', 'error', ]; export default function App() { return ( <> {statuses.map(status => ( <section key={status}> <h4>Form ({status}):</h4> <Form status={status} /> </section> ))} </> ); }
このようなページは「living styleguides」あるいは「storybooks」と呼ばれることが多いです。
Step 2: それらの状態変更を引き起こすトリガーを決定する
以下の 2 種類の入力に応答して、状態の更新をトリガーすることができます。
- Human inputs(人間からの入力)、例えばボタンをクリックする、フィールドに入力する、リンクをナビゲートするなど。
- Computer inputs(コンピュータからの入力)、例えばネットワークからのレスポンスが到着する、タイムアウトが完了する、画像が読み込まれるなど。


Illustrated by Rachel Lee Nabors
いずれの場合も、UI を更新するためには状態変数を設定する必要があります。 今回開発するフォームでは、いくつかの異なる入力に反応して状態を変更する必要があります。
- テキスト入力の変更(人間)は、テキストボックスが空かどうかによって、未入力 の状態から入力中 の状態に切り替えるか、その逆にする必要があります。
- 送信ボタンのクリック(人間)はそれを 送信中 の状態に切り替える必要があります。
- 成功したネットワーク応答(コンピュータ)はそれを 成功 状態に切り替える必要があります。
- ネットワーク応答の失敗(コンピュータ)は、対応するエラーメッセージと共に エラー 状態に切り替える必要があります。
このフローを視覚化するために、各状態をラベル付きの円として紙に描き、2 つの状態間の変化を矢印として描くことを試してみてください。このようにして多くのフローを描き出すことで、実装のはるか前にバグを整理することができます。
Form states
Step 3: useState
を使用してメモリ上に状態を表現する
次に、useState
. を使用してコンポーネントの視覚状態をメモリ内で表現する必要があります。シンプルさが鍵です。各状態は「動くパーツ」であり、可能な限り「動くパーツ」を少なくすることが望ましいです。複雑さが増すとバグも増えます!
まず絶対に必要な状態から始めます。例えば、入力の答え
を保存する必要があり、最後のエラーを保存するために(存在すれば)エラー
を保存する必要があります。
const [answer, setAnswer] = useState('');
const [error, setError] = useState(null);
そして、どの視覚を表示させるかを表す状態変数が必要になります。通常、メモリ上でそれを表現する方法は 1 つではないので、実験してみる必要があります。
もし、すぐにベストな方法が思い浮かばない場合は、まず、考えられるすべての視覚状態を確実にカバーできる程度の状態を追加することから始めてください。
const [isEmpty, setIsEmpty] = useState(true);
const [isTyping, setIsTyping] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
const [isError, setIsError] = useState(false);
最初のアイデアがベストでない可能性もありますが、それはそれで OK です。状態のリファクタリングはプロセスの一部です!
Step 4: 必要不可欠でない状態変数をすべて削除する
状態の内容に重複がないようにし、本当に必要なものだけを追跡するようにしたいです。状態の構造をリファクタリングすることに少し時間をかけることで、コンポーネントが理解しやすくなり、重複が減り、意図しない意味を持つことがなくなります。目標は、メモリ上の状態がユーザーに見せたい有効な UI を表現していないケースを防ぐことです。(例えば、エラーメッセージを表示すると同時に入力を無効化するようなことはありません。そうすると、ユーザーはエラーを修正することができなくなります!)
以下に、状態変数に関する質問をご紹介します。
- この状態だと矛盾は生じるのでしょうか? 例えば、
isTyping
とisSubmitting
の両方がtrue
であることはありません。矛盾がある状態とは通常、状態の制約が十分でないことを意味します。2 つのプール値の組み合わせは 4 通りありますが、有効な状態に対応するのは 3 つだけです。「不可能な」状態を削除するためには、これらを組み合わせて、typing
、submitting
、またはsuccess
の 3 つの値のうちの 1 つでなければならないstatus
にすればよいです。 - 同じ情報はすでに別の状態変数で利用可能ですか? もうひとつの矛盾:
isEmpty
とisTyping
は同時にtrue
であることはありません。これらを別々の状態変数にすることで、同期がとれなくなり、バグが発生する危険性があります。幸い、isEmpty
を削除して、変わりにanswer.length === 0
をチェックすることができます。 - 別の状態変数の逆数から同じ情報を得ることはできますか?
isError
は不要です、なぜなら代わりにerror !== null
をチェックできるからです。
この削減後、3 つ(7 つから減りました!)の 必須 状態変数が残ります。
const [answer, setAnswer] = useState('');
const [error, setError] = useState(null);
const [status, setStatus] = useState('typing'); // 'typing', 'submitting', or 'success'
機能性を壊さない限り、どれかを外すことはできないので、必要不可欠なものであることがわかります。
さらに深く知る
この 3 つの変数は、このフォームの状態を十分に表現しています。しかし、まだ完全に意味をなさない中間状態もあります。例えば、ステータスが success
のとき、error
が null でない状態は意味をなしません。状態をより正確にモデル化するには、reducerに抽出することができます。reducer を使えば、複数の状態変数を 1 つのオブジェクトに統一し、関連するロジックをすべて統合することができます!
Step 5: イベントハンドラーを接続して状態を設定する
最後に、状態を更新するイベントハンドラーを作成します。以下に、すべてのイベントハンドラーが接続された最終的なフォームを示します。
import { useState } from 'react'; export default function Form() { const [answer, setAnswer] = useState(''); const [error, setError] = useState(null); const [status, setStatus] = useState('typing'); if (status === 'success') { return <h1>That's right!</h1> } async function handleSubmit(e) { e.preventDefault(); setStatus('submitting'); try { await submitForm(answer); setStatus('success'); } catch (err) { setStatus('typing'); setError(err); } } function handleTextareaChange(e) { setAnswer(e.target.value); } return ( <> <h2>City quiz</h2> <p> In which city is there a billboard that turns air into drinkable water? </p> <form onSubmit={handleSubmit}> <textarea value={answer} onChange={handleTextareaChange} disabled={status === 'submitting'} /> <br /> <button disabled={ answer.length === 0 || status === 'submitting' }> Submit </button> {error !== null && <p className="Error"> {error.message} </p> } </form> </> ); } function submitForm(answer) { // Pretend it's hitting the network. return new Promise((resolve, reject) => { setTimeout(() => { let shouldError = answer.toLowerCase() !== 'lima' if (shouldError) { reject(new Error('Good guess but a wrong answer. Try again!')); } else { resolve(); } }, 1500); }); }
このコードは、元の命令型の例よりも長くなっていますが、はるかに壊れにくくなっています。すべてのインタラクションを状態変化として表現することで、既存の状態を壊すことなく、後から新しい視覚状態を導入することができます。また、インタラクション自体のロジックを変更することなく、各状態で表示されるべきものを変更することができます。
まとめ
- 宣言的プログラミングとは、UI を細かく管理する(命令的)のではなく、視覚状態ごとに UI を記述することを意味します。
- コンポーネントを開発するとき:
- コンポーネントの視覚状態をすべて特定する。
- 状態を変更するための人間およびコンピュータのトリガーを決定する。
useState
で状態をモデル化する。- バグや矛盾を避けるため、不必要な状態を削除する。
- 状態を設定するためのイベントハンドラーを接続する。
チャレンジ 1/3: CSS クラスの追加・削除
画像をクリックすると、外側の <div>
から background--active
CSS クラスが削除され、<img>
に picture--active
クラスが追加されるようにしてください。もう一度背景をクリックすると、元の CSS クラスに戻るようにします。
視覚的には、画像の上をクリックすると、紫色の背景が消え、画像の境界線が強調されると考えてください。画像の外側をクリックすると、背景が強調されますが、画像の境界線の強調は削除されます。
export default function Picture() { return ( <div className="background background--active"> <img className="picture" alt="Rainbow houses in Kampung Pelangi, Indonesia" src="https://i.imgur.com/5qwVYb1.jpeg" /> </div> ); }