state構造の選択
stateを適切に構造化することは、修正やデバッグが容易なコンポーネントと、常にバグの原因となるコンポーネントの違いを生みます。stateを構造化する際に考慮すべきいくつかのヒントを紹介します。
学習内容
- 単一のstate変数と複数のstate変数の使い分け
- stateを整理する際に避けるべきこと
- state構造に関する一般的な問題の修正方法
state構造化の原則
stateを持つコンポーネントを記述する際には、使用するstate変数の数とそのデータの形状について選択を行う必要があります。最適ではないstate構造でも正しいプログラムを書くことは可能ですが、より良い選択をするためのいくつかの原則があります:
- 関連するstateをグループ化する。2つ以上のstate変数を常に同時に更新する場合は、それらを単一のstate変数に統合することを検討してください。
- state内の矛盾を避ける。複数のstateが互いに矛盾し、「不一致」を起こす可能性のある方法でstateが構造化されている場合、ミスが発生する余地が生まれます。これを避けるようにしてください。
- 冗長なstateを避ける。コンポーネントのpropsや既存のstate変数からレンダリング中に情報を計算できる場合、その情報をコンポーネントのstateに入れるべきではありません。
- state内の重複を避ける。同じデータが複数のstate変数間、またはネストされたオブジェクト内で重複している場合、それらを同期させ続けることが困難になります。可能な場合は重複を減らしてください。
- 深くネストされたstateを避ける。深い階層構造を持つstateは更新が非常に不便です。可能な場合は、stateを平坦な方法で構造化することを優先してください。
これらの原則の背後にある目標は、ミスを導入せずにstateを簡単に更新できるようにすることです。stateから冗長なデータや重複したデータを削除することで、すべての部分が同期された状態を保つことができます。これは、データベースエンジニアがバグの可能性を減らすためにデータベース構造を「正規化」したいと思うのと似ています。アルベルト・アインシュタインの言葉を借りれば、「stateを可能な限りシンプルに、しかしそれ以上に単純にはしないこと」
では、これらの原則が実際にどのように適用されるかを見てみましょう。
関連するstateをグループ化する
単一のstate変数を使用するか、複数のstate変数を使用するかについて、迷うことがあるかもしれません。
次のようにすべきでしょうか?
それとも、このように?
技術的には、どちらのアプローチも使用できます。しかし、2つのstate変数が常に一緒に変化する場合は、それらを単一のstate変数に統一するのが良い考えかもしれません。そうすれば、以下の例のように、カーソルの移動で赤い点の両方の座標を更新する際に、常にそれらを同期させることを忘れることはありません:
データをオブジェクトや配列にグループ化するもう一つのケースは、必要なstateの数がわからない場合です。例えば、ユーザーがカスタムフィールドを追加できるフォームがある場合に役立ちます。
落とし穴
state変数がオブジェクトの場合、他のフィールドを明示的にコピーせずに単一フィールドのみを更新することはできないことを覚えておいてください。例えば、上記の例ではsetPosition({ x: 100 })を行うことはできません。なぜなら、それにはyプロパティが全く含まれないからです!代わりに、xだけを設定したい場合は、setPosition({ ...position, x: 100 })を行うか、2つのstate変数に分割してsetX(100)を行います。
state内の矛盾を避ける
以下は、isSendingとisSentというstate変数を持つホテルのフィードバックフォームです:
このコードは動作しますが、「不可能な」状態への扉を開いたままです。例えば、setIsSentとsetIsSendingを一緒に呼び出すのを忘れると、isSendingとisSentが両方ともtrueになる状況に陥る可能性があります。コンポーネントが複雑になるほど、何が起こったのかを理解するのが難しくなります。
なぜなら、isSendingとisSentが同時にtrueになるべきではないからです。これらを、statusという1つの状態変数に置き換え、3つの有効な状態のいずれかを取るようにする方が良いでしょう:'typing'(初期状態)、'sending'、そして'sent'です:
可読性のためにいくつかの定数を宣言することもできます:
しかし、これらは状態変数ではないため、互いに同期が取れなくなることを心配する必要はありません。
冗長な状態を避ける
コンポーネントのpropsや既存の状態変数から、レンダリング中に何らかの情報を計算できる場合、その情報をそのコンポーネントの状態に入れるべきではありません。
例えば、このフォームを見てください。これは動作しますが、冗長な状態を見つけられますか?
このフォームには3つの状態変数があります:firstName、lastName、そしてfullNameです。しかし、fullNameは冗長です。レンダリング中にfullNameをfirstNameとlastNameから常に計算できるため、状態から削除してください。
これはその方法です:
ここでは、fullNameは状態変数ではありません。代わりに、レンダリング中に計算されます:
その結果、変更ハンドラはそれを更新するために特別なことをする必要はありません。setFirstNameまたはsetLastNameを呼び出すと、再レンダリングがトリガーされ、次のfullNameが新しいデータから計算されます。
stateでの重複を避ける
このメニューリストコンポーネントでは、いくつかの旅行用スナックから1つを選ぶことができます:
現在、選択されたアイテムはselectedItemstate変数にオブジェクトとして保存されています。しかし、これは良くありません:なぜなら、selectedItemの内容は、itemsリスト内のアイテムの1つと同じオブジェクトだからです。つまり、アイテム自体に関する情報が2か所で重複していることになります。
なぜこれが問題なのでしょうか?各アイテムを編集可能にしてみましょう:
まずアイテムの「Choose」をクリックし、その後編集すると、入力は更新されますが、下部のラベルは編集を反映しないことに注意してください。これはstateが重複しており、selectedItemの更新を忘れているためです。
確かにselectedItemも更新することはできますが、より簡単な修正は重複を排除することです。この例では、selectedItemオブジェクト(これはitems内のオブジェクトとの重複を生みます)の代わりに、stateにselectedIdを保持し、その後、そのIDを持つアイテムをitems配列から検索してselectedItemを取得します:
以前、状態は次のように重複していました:
items = [{ id: 0, title: 'pretzels'}, ...]selectedItem = {id: 0, title: 'pretzels'}
しかし、変更後は次のようになります:
items = [{ id: 0, title: 'pretzels'}, ...]selectedId = 0
重複がなくなり、本質的な状態のみを保持しています!
これで、選択されたアイテムを編集すると、下のメッセージが即座に更新されます。これは、setItemsが再レンダリングをトリガーし、items.find(...)が更新されたタイトルのアイテムを見つけるためです。選択されたアイテムを状態として保持する必要はありませんでした。なぜなら、本質的なのは選択されたIDだけだからです。残りはレンダリング中に計算できます。
深くネストされた状態を避ける
惑星、大陸、国からなる旅行計画を想像してみてください。この例のように、ネストされたオブジェクトや配列を使用して状態を構造化したくなるかもしれません:
さて、すでに訪れた場所を削除するボタンを追加したいとします。どのように進めますか?ネストされた状態の更新には、変更された部分から上に向かってオブジェクトのコピーを作成することが含まれます。深くネストされた場所を削除するには、その親の場所のチェーン全体をコピーする必要があります。このようなコードは非常に冗長になる可能性があります。
状態がネストされすぎて更新が難しい場合は、「フラット」にすることを検討してください。ここでは、このデータを再構築する方法を1つ紹介します。各placeがその子の場所の配列を持つツリーのような構造ではなく、各場所がその子の場所のIDの配列を保持するようにします。そして、各場所IDから対応する場所へのマッピングを保存します。
このデータの再構築は、データベーステーブルを見ていることを思い起こさせるかもしれません:
これで状態が「フラット」(「正規化」とも呼ばれます)になったため、ネストされた項目の更新が容易になります。
場所を削除するには、状態の2つのレベルを更新するだけで済みます:
- その親場所の更新版は、削除されたIDを
childIds配列から除外する必要があります。 - ルートの「テーブル」オブジェクトの更新版には、親場所の更新版を含める必要があります。
その方法の例を以下に示します:
状態をいくらでもネストすることはできますが、「平坦」にすることで多くの問題を解決できます。状態の更新が容易になり、ネストされたオブジェクトの異なる部分に重複がないことを保証するのに役立ちます。
場合によっては、ネストされた状態の一部を子コンポーネントに移動することで、状態のネストを減らすこともできます。これは、アイテムがホバーされているかどうかなど、保存する必要のない一時的なUI状態に適しています。
まとめ
- 2つの状態変数が常に一緒に更新される場合は、それらを1つにまとめることを検討してください。
- 「不可能な」状態を作成しないように、状態変数を慎重に選択してください。
- 状態を更新する際にミスをする可能性を減らすように状態を構造化してください。
- 冗長で重複した状態を避け、同期を取る必要がないようにしてください。
- 特に更新を防ぎたい場合を除き、propsを状態に入れないでください。
- 選択のようなUIパターンでは、オブジェクト自体ではなく、IDやインデックスを状態に保持してください。
- 深くネストされた状態の更新が複雑な場合は、平坦化してみてください。
Try out some challenges
Challenge 1 of 4:Fix a component that’s not updating #
This Clock component receives two props: color and time. When you select a different color in the select box, the Clock component receives a different color prop from its parent component. However, for some reason, the displayed color doesn’t update. Why? Fix the problem.
