v19.2Latest

Stateによる入力への反応

ReactはUIを操作する宣言的な方法を提供します。UIの個々の部分を直接操作する代わりに、コンポーネントが取り得る様々な状態を記述し、ユーザーの入力に応じてそれらを切り替えます。これはデザイナーがUIについて考える方法に似ています。

学習内容
  • 宣言的UIプログラミングが命令的UIプログラミングとどのように異なるか
  • コンポーネントが取り得る様々な視覚的状態を列挙する方法
  • コードから異なる視覚的状態間の変更をトリガーする方法

宣言的UIと命令的UIの比較

UIインタラクションを設計するとき、おそらくUIがユーザーの操作に応じてどのように変化するかを考えるでしょう。ユーザーが回答を送信できるフォームを考えてみてください:

  • フォームに何かを入力すると、「送信」ボタンが有効になります。
  • 「送信」を押すと、フォームとボタンの両方が無効になり、スピナーが表示されます。
  • ネットワークリクエストが成功すると、フォームが非表示になり、「ありがとうございます」メッセージが表示されます。
  • ネットワークリクエストが失敗すると、エラーメッセージが表示され、フォームが再び有効になります。

命令的プログラミングでは、上記はインタラクションの実装方法に直接対応します。何が起こったかに応じてUIを操作するための正確な指示を書かなければなりません。別の考え方としては、車に乗っている隣の人に、曲がる場所を一つ一つ指示しながら目的地まで案内することを想像してください。

JavaScriptを表す不安そうな顔のドライバーが運転する車の中で、乗客が複雑な曲がり角ごとのナビゲーションのシーケンスを実行するようドライバーに指示している様子。

イラスト:Rachel Lee Nabors

彼らはあなたがどこに行きたいのか知らず、ただあなたの命令に従います。(そして、もし道順を間違えたら、間違った場所に到着してしまいます!)これは、スピナーからボタンまでの各要素に「命令」し、コンピューターにUIをどのように更新するかを伝えなければならないため、命令的と呼ばれます。

この命令的UIプログラミングの例では、フォームはReactなしで構築されています。ブラウザのDOMのみを使用しています:

UIを命令的に操作することは、独立した例では十分に機能しますが、より複雑なシステムでは管理が指数関数的に難しくなります。このようなフォームでいっぱいのページを更新することを想像してみてください。新しいUI要素や新しいインタラクションを追加するには、既存のコードを注意深くチェックして、バグ(例えば、何かを表示または非表示にするのを忘れるなど)を導入していないことを確認する必要があります。

Reactはこの問題を解決するために構築されました。

Reactでは、UIを直接操作しません—つまり、コンポーネントを直接有効化、無効化、表示、非表示にすることはありません。代わりに、何を表示したいかを宣言し、ReactがUIを更新する方法を考え出します。タクシーに乗り、どこで曲がるかを正確に指示するのではなく、どこに行きたいかを運転手に伝えることを考えてください。あなたをそこに連れて行くのは運転手の仕事であり、彼らはあなたが考えもしなかった近道を知っているかもしれません!

Reactが運転する車の中で、乗客が地図上の特定の場所に連れて行ってくれるよう頼んでいる。Reactはそれをどうやって行うかを考える。

イラスト:Rachel Lee Nabors

UIを宣言的に考える

上記でフォームを命令的に実装する方法を見てきました。Reactでの考え方をよりよく理解するために、以下でこのUIをReactで再実装する手順を説明します:

  1. 特定する:コンポーネントの異なる視覚的状態を
  2. 決定する:それらの状態変化を引き起こす要因を
  3. 表現する:メモリ内の状態をuseState
  4. 削除する:不要な状態変数を
  5. 接続する:状態を設定するイベントハンドラを

ステップ1: コンポーネントの異なる視覚的状態を特定する

コンピュータサイエンスでは、「状態機械」がいくつかの「状態」のうちの一つにある、という話を聞くことがあるかもしれません。デザイナーと一緒に仕事をしているなら、異なる「視覚的状態」のためのモックアップを見たことがあるかもしれません。Reactはデザインとコンピュータサイエンスの交差点に立っているため、これらの両方の考え方がインスピレーションの源となります。

まず、ユーザーが見る可能性のあるUIのすべての異なる「状態」を視覚化する必要があります:

  • :フォームには無効な「送信」ボタンがある。
  • 入力中:フォームには有効な「送信」ボタンがある。
  • 送信中:フォームは完全に無効化されている。スピナーが表示される。
  • 成功:フォームの代わりに「ありがとう」メッセージが表示される。
  • エラー:「入力中」状態と同じだが、追加のエラーメッセージがある。

デザイナーと同じように、ロジックを追加する前に、異なる状態のための「モックアップ」を作成したり「モック」を作成したりしたいと思うでしょう。例えば、これはフォームの視覚的な部分だけのモックです。このモックは、デフォルト値がstatusという名前のpropによって制御されています:'empty'

そのpropには好きな名前を付けることができ、命名は重要ではありません。status = 'empty'status = 'success'に編集して、成功メッセージが表示されるのを確認してみてください。モッキングにより、ロジックを接続する前にUIを素早く反復することができます。以下は、同じコンポーネントのより詳細なプロトタイプで、やはりstatuspropによって「制御」されています:

ステップ2: それらの状態変化を引き起こす要因を決定する

状態の更新は、2種類の入力に応答して引き起こすことができます:

  • 人間の入力:ボタンのクリック、フィールドへの入力、リンクのナビゲートなど。
  • コンピュータの入力:ネットワーク応答の到着、タイムアウトの完了、画像の読み込みなど。
指1と0

イラスト:Rachel Lee Nabors

どちらの場合も、UIを更新するには状態変数を設定する必要があります。開発中のフォームでは、いくつかの異なる入力に応答して状態を変更する必要があります:

  • テキスト入力の変更(人間による)は、テキストボックスが空かどうかに応じて、Empty状態からTyping状態へ、またはその逆へ切り替えるべきです。
  • Submitボタンのクリック(人間による)は、Submitting状態へ切り替えるべきです。
  • ネットワーク応答の成功(コンピュータによる)は、Success状態へ切り替えるべきです。
  • ネットワーク応答の失敗(コンピュータによる)は、対応するエラーメッセージとともにError状態へ切り替えるべきです。
注意

人間による入力には、しばしばイベントハンドラが必要であることに注意してください!

このフローを視覚化するために、各状態をラベル付きの円として、2つの状態間の各変化を矢印として紙に描いてみてください。この方法で多くのフローをスケッチし、実装のずっと前にバグを整理することができます。

左から右へ移動するフローチャートで、5つのノードがあります。最初のノードは'empty'とラベル付けされており、'start typing'とラベル付けされた1つのエッジが'typing'とラベル付けされたノードに接続されています。そのノードには'press submit'とラベル付けされた1つのエッジがあり、'submitting'とラベル付けされたノードに接続されています。このノードには2つのエッジがあります。左側のエッジは'network error'とラベル付けされ、'error'とラベル付けされたノードに接続されています。右側のエッジは'network success'とラベル付けされ、'success'とラベル付けされたノードに接続されています。左から右へ移動するフローチャートで、5つのノードがあります。最初のノードは'empty'とラベル付けされており、'start typing'とラベル付けされた1つのエッジが'typing'とラベル付けされたノードに接続されています。そのノードには'press submit'とラベル付けされた1つのエッジがあり、'submitting'とラベル付けされたノードに接続されています。このノードには2つのエッジがあります。左側のエッジは'network error'とラベル付けされ、'error'とラベル付けされたノードに接続されています。右側のエッジは'network success'とラベル付けされ、'success'とラベル付けされたノードに接続されています。

フォームの状態

ステップ3: useStateでメモリ内に状態を表現するuseState

次に、コンポーネントの視覚的な状態をメモリ内でuseStateで表現する必要があります。シンプルさが鍵です:各状態は「動くピース」であり、できるだけ少ない「動くピース」にしたいものです。複雑さが増すと、バグも増えます!

まず、絶対に必要な状態から始めます。例えば、入力のためのanswerと、最後のエラーを保存するためのerror(存在する場合)を保存する必要があります:

次に、表示したい視覚的な状態のうちのどれかを表す状態変数が必要です。これをメモリ内で表現する方法は通常複数あるので、試行錯誤する必要があります。

最善の方法がすぐに思いつかない場合は、すべての可能な視覚的な状態が確実にカバーされるように、十分な状態を追加することから始めてください:

最初のアイデアが最善でなくても問題ありません。状態のリファクタリングはプロセスの一部です!

ステップ4: 不要な状態変数を削除する

状態内容の重複を避け、本質的なものだけを追跡したいものです。状態構造のリファクタリングに少し時間をかけることで、コンポーネントの理解が容易になり、重複が減り、意図しない意味を避けることができます。目標は、メモリ内の状態が、ユーザーに見せたい有効なUIを表現していないケースを防ぐことです。(例えば、エラーメッセージを表示しながら入力を無効化することは絶対に避けたいものです。そうするとユーザーはエラーを修正できなくなります!)

状態変数について自問できるいくつかの質問を以下に示します:

  • この状態はパラドックスを引き起こしますか? 例えば、isTypingisSubmittingが両方ともtrueになることはできません。パラドックスは通常、状態が十分に制約されていないことを意味します。2つのブール値には4つの可能な組み合わせがありますが、有効な状態に対応するのは3つだけです。この「不可能な」状態を取り除くには、これらをstatusという3つの値のいずれかでなければならないものに結合できます:'typing''submitting'、または'success'
  • 同じ情報が他の状態変数ですでに利用可能ですか?別のパラドックス:isEmptyisTypingが同時にtrueになることはできません。これらを別々の状態変数にすると、同期が取れなくなり、バグが発生するリスクがあります。幸いなことに、isEmptyを削除し、代わりにanswer.length === 0をチェックできます。
  • 他の状態変数の逆から同じ情報を得られますか?isErrorは不要です。なぜなら、代わりにerror !== nullをチェックできるからです。

この整理の後、残るのは3つの(7つから減りました!)本質的な状態変数です:

これらが本質的であることは、どれかを削除すると機能が壊れることからわかります。

Deep Dive
リデューサーで「不可能な」状態を排除する

ステップ5:状態を設定するイベントハンドラーを接続する

最後に、状態を更新するイベントハンドラーを作成します。以下は、すべてのイベントハンドラーが接続された最終的なフォームです:

このコードは元の命令的な例よりも長いですが、はるかに脆弱ではありません。すべてのインタラクションを状態の変化として表現することで、後で新しい視覚的状態を導入しても既存の状態を壊さずに済みます。また、各状態で何を表示すべきかを変更する際に、インタラクション自体のロジックを変更する必要がありません。

まとめ

  • 宣言的プログラミングとは、UIを細かく管理する(命令的)のではなく、各視覚的状態に対してUIを記述することを意味します。
  • コンポーネントを開発する際には:
    1. すべての視覚的状態を特定します。
    2. 状態変化の人間およびコンピューターのトリガーを決定します。
    3. 状態をuseStateでモデル化します。
    4. バグやパラドックスを避けるために、本質的でない状態を削除します。
    5. 状態を設定するイベントハンドラーを接続します。

Try out some challenges

Challenge 1 of 3:Add and remove a CSS class #

Make it so that clicking on the picture removes the background--active CSS class from the outer <div>, but adds the picture--active class to the <img>. Clicking the background again should restore the original CSS classes.

Visually, you should expect that clicking on the picture removes the purple background and highlights the picture border. Clicking outside the picture highlights the background, but removes the picture border highlight.