v19.2Latest

状態の保持とリセット

状態はコンポーネント間で独立しています。Reactは、UIツリー内の位置に基づいて、どの状態がどのコンポーネントに属するかを追跡します。再レンダリング間で状態をいつ保持し、いつリセットするかを制御できます。

学習内容
  • Reactが状態を保持またはリセットするタイミング
  • コンポーネントの状態を強制的にリセットする方法
  • キーとタイプが状態の保持に与える影響

状態はレンダーツリー内の位置に結びついている

Reactは、UI内のコンポーネント構造に対してレンダーツリーを構築します。

コンポーネントに状態を与えると、状態がコンポーネントの「内部」に「存在」すると考えるかもしれません。しかし、状態は実際にはReact内部で保持されています。Reactは、保持している各状態の断片を、そのコンポーネントがレンダーツリー内のどこに位置するかによって、正しいコンポーネントに関連付けます。

ここでは、<Counter />というJSXタグは1つだけですが、2つの異なる位置でレンダリングされています:

これらをツリーとして表すと次のようになります:

Reactコンポーネントのツリー図。ルートノードは'div'とラベル付けされており、2つの子を持っています。各子は'Counter'とラベル付けされており、両方とも値0の'count'というラベルの状態バブルを含んでいます。Reactコンポーネントのツリー図。ルートノードは'div'とラベル付けされており、2つの子を持っています。各子は'Counter'とラベル付けされており、両方とも値0の'count'というラベルの状態バブルを含んでいます。

Reactツリー

これらはツリー内のそれぞれ独自の位置でレンダリングされるため、2つの独立したカウンターです。Reactを使用する際にこれらの位置について考える必要は通常ありませんが、その仕組みを理解しておくと役立つことがあります。

Reactでは、画面上の各コンポーネントは完全に独立した状態を持ちます。例えば、2つのCounterコンポーネントを並べてレンダリングすると、それぞれが独自の独立したscore状態とhover状態を持ちます。

両方のカウンターをクリックしてみて、互いに影響を与えないことを確認してください:

ご覧の通り、一方のカウンターが更新されると、そのコンポーネントの状態のみが更新されます:

Reactコンポーネントのツリー図。ルートノードは'div'とラベル付けされており、2つの子を持っています。左の子は'Counter'とラベル付けされており、値0の'count'というラベルの状態バブルを含んでいます。右の子は'Counter'とラベル付けされており、値1の'count'というラベルの状態バブルを含んでいます。右の子の状態バブルは、値が更新されたことを示すために黄色でハイライトされています。Reactコンポーネントのツリー図。ルートノードは'div'とラベル付けされており、2つの子を持っています。左の子は'Counter'とラベル付けされており、値0の'count'というラベルの状態バブルを含んでいます。右の子は'Counter'とラベル付けされており、値1の'count'というラベルの状態バブルを含んでいます。右の子の状態バブルは、値が更新されたことを示すために黄色でハイライトされています。

状態の更新

Reactは、同じコンポーネントをツリー内の同じ位置でレンダリングし続ける限り、状態を保持します。これを確認するには、両方のカウンターをインクリメントし、「2番目のカウンターをレンダリング」チェックボックスのチェックを外して2番目のコンポーネントを削除し、その後チェックを入れて再度追加してください:

2番目のカウンターのレンダリングを停止すると、その状態が完全に消えることに注目してください。これは、Reactがコンポーネントを削除する際に、その状態も破棄するためです。

Reactコンポーネントのツリー図。ルートノードは'div'とラベル付けされており、2つの子を持っています。左の子は'Counter'とラベル付けされており、値0の'count'というラベルの状態バブルを含んでいます。右の子は存在せず、その場所には黄色の'poof'画像があり、ツリーからコンポーネントが削除されていることを強調しています。Reactコンポーネントツリーの図。ルートノードは'div'とラベル付けされており、2つの子を持っています。左の子は'Counter'とラベル付けされ、値0の'count'というラベルの状態バブルを含んでいます。右の子はなく、その場所には黄色の'poof'画像があり、ツリーから削除されたコンポーネントが強調されています。

コンポーネントの削除

「2つ目のカウンターをレンダリングする」にチェックを入れると、2つ目のCounterとその状態がゼロから初期化され(score = 0)、DOMに追加されます。

Reactコンポーネントツリーの図。ルートノードは'div'とラベル付けされており、2つの子を持っています。左の子は'Counter'とラベル付けされ、値0の'count'というラベルの状態バブルを含んでいます。右の子は'Counter'とラベル付けされ、値0の'count'というラベルの状態バブルを含んでいます。右の子ノード全体が黄色で強調表示されており、ツリーに追加されたばかりであることを示しています。Reactコンポーネントツリーの図。ルートノードは'div'とラベル付けされており、2つの子を持っています。左の子は'Counter'とラベル付けされ、値0の'count'というラベルの状態バブルを含んでいます。右の子は'Counter'とラベル付けされ、値0の'count'というラベルの状態バブルを含んでいます。右の子ノード全体が黄色で強調表示されており、ツリーに追加されたばかりであることを示しています。

コンポーネントの追加

Reactは、コンポーネントがUIツリー内のその位置でレンダリングされている限り、その状態を保持します。コンポーネントが削除されるか、同じ位置に別のコンポーネントがレンダリングされると、Reactはその状態を破棄します。

同じ位置にある同じコンポーネントは状態を保持する

この例では、2つの異なる<Counter />タグがあります:

チェックボックスをオンまたはオフにしても、カウンターの状態はリセットされません。isFancytrueであろうとfalseであろうと、ルートのAppコンポーネントから返されるdivの最初の子として、常に<Counter />があります:

矢印で遷移する2つのセクションに分かれた図。各セクションには、親が'App'とラベル付けされ、isFancyというラベルの状態バブルを含むコンポーネントのレイアウトが含まれています。このコンポーネントには'div'とラベル付けされた1つの子があり、それが唯一の子に渡されるisFancy(紫色で強調表示)を含むpropsバブルにつながります。最後の子は'Counter'とラベル付けされ、両方の図で'count'というラベルと値3の状態バブルを含んでいます。図の左側のセクションでは何も強調表示されておらず、isFancy親状態の値はfalseです。図の右側のセクションでは、isFancy親状態の値がtrueに変更されて黄色で強調表示されており、その下のpropsバブルもisFancy値をtrueに変更して黄色で強調表示されています。矢印で遷移する2つのセクションに分かれた図。各セクションには、親が'App'とラベル付けされ、isFancyというラベルの状態バブルを含むコンポーネントのレイアウトが含まれています。このコンポーネントには'div'とラベル付けされた1つの子があり、それが唯一の子に渡されるisFancy(紫色で強調表示)を含むpropsバブルにつながります。最後の子は'Counter'とラベル付けされ、両方の図で'count'というラベルと値3の状態バブルを含んでいます。図の左側のセクションでは何も強調表示されておらず、isFancy親状態の値はfalseです。図の右側のセクションでは、isFancy親状態の値がtrueに変更されて黄色で強調表示されており、その下のpropsバブルもisFancy値をtrueに変更して黄色で強調表示されています。

Appの状態を更新してもCounterはリセットされないCounterが同じ位置にあるため

同じ位置にある同じコンポーネントなので、Reactの視点では同じカウンターです。

落とし穴

覚えておいてください、Reactにとって重要なのはJSXマークアップ内の位置ではなく、UIツリー内の位置です!このコンポーネントには、ifの内側と外側に異なる<Counter />JSXタグを持つ2つのreturn節があります:

チェックボックスをオンにすると状態がリセットされると思うかもしれませんが、そうはなりません!これは、これらの両方の<Counter />タグが同じ位置でレンダリングされるためです。Reactは、あなたが関数内のどこに条件を配置したかは知りません。Reactが「見る」のは、あなたが返すツリーだけです。

どちらの場合も、Appコンポーネントは、<Counter />を最初の子として持つ<div>を返します。Reactにとって、これら2つのカウンターは同じ「アドレス」を持っています:ルートの最初の子の最初の子。これが、ロジックをどのように構造化しても、Reactが前回と次のレンダリングの間でそれらを照合する方法です。

同じ位置にある異なるコンポーネントは状態をリセットする

この例では、チェックボックスをオンにすると<Counter><p>に置き換わります:

ここでは、同じ位置で異なるコンポーネントタイプを切り替えています。最初は、<div>の最初の子要素にCounterが含まれていました。しかし、pに置き換えたとき、ReactはUIツリーからCounterを削除し、その状態を破棄しました。

3つのセクションからなる図で、各セクションの間に遷移を示す矢印がある。最初のセクションには、'div'とラベル付けされたReactコンポーネントがあり、'Counter'とラベル付けされた単一の子要素を持つ。この子要素には値3の'count'という状態バブルが含まれている。中間のセクションには同じ'div'親があるが、子コンポーネントは削除済みで、黄色の'proof'画像で示されている。3番目のセクションには再び同じ'div'親があり、今度は黄色でハイライトされた'p'というラベルの新しい子要素を持つ。3つのセクションからなる図で、各セクションの間に遷移を示す矢印がある。最初のセクションには、'div'とラベル付けされたReactコンポーネントがあり、'Counter'とラベル付けされた単一の子要素を持つ。この子要素には値3の'count'という状態バブルが含まれている。中間のセクションには同じ'div'親があるが、子コンポーネントは削除済みで、黄色の'proof'画像で示されている。3番目のセクションには再び同じ'div'親があり、今度は黄色でハイライトされた'p'というラベルの新しい子要素を持つ。

Counterpに変わると、Counterは削除され、pが追加されます

3つのセクションからなる図で、各セクションの間に遷移を示す矢印がある。最初のセクションには、'p'とラベル付けされたReactコンポーネントがある。中間のセクションには同じ'div'親があるが、子コンポーネントは削除済みで、黄色の'proof'画像で示されている。3番目のセクションには再び同じ'div'親があり、今度は黄色でハイライトされた'Counter'というラベルの新しい子要素を持つ。この子要素には値0の'count'という状態バブルが含まれている。3つのセクションからなる図で、各セクションの間に遷移を示す矢印がある。最初のセクションには、'p'とラベル付けされたReactコンポーネントがある。中間のセクションには同じ'div'親があるが、子コンポーネントは削除済みで、黄色の'proof'画像で示されている。3番目のセクションには再び同じ'div'親があり、今度は黄色でハイライトされた'Counter'というラベルの新しい子要素を持つ。この子要素には値0の'count'という状態バブルが含まれている。

戻すと、pが削除され、Counterが追加されます

また、同じ位置で異なるコンポーネントをレンダリングすると、そのサブツリー全体の状態がリセットされます。これがどのように機能するかを確認するには、カウンターを増やしてからチェックボックスをオンにしてください:

チェックボックスをクリックすると、カウンターの状態がリセットされます。Counterをレンダリングしていますが、divの最初の子要素がsectionからdivに変わります。子要素のsectionがDOMから削除されると、その下のツリー全体(Counterとその状態を含む)も破棄されます。

3つのセクションからなる図で、各セクションの間に遷移を示す矢印がある。最初のセクションには、'div'とラベル付けされたReactコンポーネントがあり、'section'とラベル付けされた単一の子要素を持つ。この子要素には、値3の'count'という状態バブルを含む'Counter'というラベルの単一の子要素がある。中間のセクションには同じ'div'親があるが、子コンポーネントは削除済みで、黄色の'proof'画像で示されている。3番目のセクションには再び同じ'div'親があり、今度は黄色でハイライトされた'div'というラベルの新しい子要素を持つ。この子要素には、値0の'count'という状態バブルを含む'Counter'というラベルの新しい子要素もあり、すべて黄色でハイライトされている。3つのセクションからなる図で、各セクションの間に遷移を示す矢印がある。最初のセクションには、'div'とラベル付けされたReactコンポーネントがあり、'section'とラベル付けされた単一の子要素を持つ。この子要素には、値3の'count'という状態バブルを含む'Counter'というラベルの単一の子要素がある。中間のセクションには同じ'div'親があるが、子コンポーネントは削除済みで、黄色の'proof'画像で示されている。3番目のセクションには再び同じ'div'親があり、今度は黄色でハイライトされた'div'というラベルの新しい子要素を持つ。この子要素には、値0の'count'という状態バブルを含む'Counter'というラベルの新しい子要素もあり、すべて黄色でハイライトされている。

sectiondivに変わると、sectionは削除され、新しいdivが追加されます

3つのセクションからなる図。各セクション間を矢印が遷移している。最初のセクションには、単一の子要素'div'を持つReactコンポーネント'div'が含まれ、その子要素'div'には単一の子要素'Counter'があり、状態バブル'count'(値0)を含んでいる。中央のセクションには同じ親'div'があるが、子コンポーネントは削除されており、黄色の'proof'画像で示されている。3番目のセクションには再び同じ親'div'があり、黄色でハイライトされた新しい子要素'section'と、状態バブル'count'(値0)を含む新しい子要素'Counter'が黄色でハイライトされている。3つのセクションからなる図。各セクション間を矢印が遷移している。最初のセクションには、単一の子要素'div'を持つReactコンポーネント'div'が含まれ、その子要素'div'には単一の子要素'Counter'があり、状態バブル'count'(値0)を含んでいる。中央のセクションには同じ親'div'があるが、子コンポーネントは削除されており、黄色の'proof'画像で示されている。3番目のセクションには再び同じ親'div'があり、黄色でハイライトされた新しい子要素'section'と、状態バブル'count'(値0)を含む新しい子要素'Counter'が黄色でハイライトされている。

戻り切り替え時、divが削除され、新しいsectionが追加されます。

経験則として、再レンダリング間で状態を保持したい場合、ツリーの構造がレンダリング間で「一致」する必要があります。構造が異なる場合、Reactはツリーからコンポーネントを削除する際に状態を破棄するため、状態は失われます。

落とし穴

これが、コンポーネント関数の定義をネストすべきではない理由です。

ここでは、MyTextFieldコンポーネント関数が内部MyComponentで定義されています:

ボタンをクリックするたびに、入力状態が消えます!これは、異なるMyTextField関数がMyComponentのレンダリングごとに作成されるためです。同じ位置に異なるコンポーネントをレンダリングしているため、Reactはその下のすべての状態をリセットします。これはバグやパフォーマンス問題を引き起こします。この問題を避けるには、コンポーネント関数は常にトップレベルで宣言し、定義をネストしないでください。

同じ位置での状態のリセット

デフォルトでは、Reactはコンポーネントが同じ位置にある間、その状態を保持します。通常、これはまさに望ましい動作であり、デフォルトの動作として理にかなっています。しかし、時にはコンポーネントの状態をリセットしたい場合があります。各ターン中に2人のプレイヤーがスコアを記録できるこのアプリを考えてみましょう:

現在、プレイヤーを変更してもスコアは保持されます。2つのCounterは同じ位置に表示されるため、Reactはそれらを同じCounterであり、personプロップが変更されたものと見なします。

しかし概念的には、このアプリではこれらは2つの独立したカウンターであるべきです。UI上では同じ場所に表示されるかもしれませんが、一方はTaylor用のカウンター、もう一方はSarah用のカウンターです。

これらを切り替える際に状態をリセットする方法は2つあります:

  1. コンポーネントを異なる位置でレンダリングする
  2. 各コンポーネントにkeyで明示的な識別子を与える

オプション1:コンポーネントを異なる位置でレンダリングする

これら2つのCounterを独立させたい場合は、2つの異なる位置でレンダリングできます:

  • 最初、isPlayerAtrueです。したがって、最初の位置にはCounterの状態が含まれ、2番目の位置は空です。
  • 「Next player」ボタンをクリックすると、最初の位置はクリアされますが、2番目の位置にCounterが含まれるようになります。
Reactコンポーネントのツリー図。親は'Scoreboard'とラベル付けされ、値が'true'のisPlayerAという状態バブルを持っています。唯一の子要素は左側に配置され、'count'というラベルと値0を持つ状態バブルを持つ'Counter'とラベル付けされています。左側の子要素全体が黄色でハイライトされており、追加されたことを示しています。Reactコンポーネントのツリー図。親は'Scoreboard'とラベル付けされ、値が'true'のisPlayerAという状態バブルを持っています。唯一の子要素は左側に配置され、'count'というラベルと値0を持つ状態バブルを持つ'Counter'とラベル付けされています。左側の子要素全体が黄色でハイライトされており、追加されたことを示しています。

初期状態

Reactコンポーネントのツリー図。親は'Scoreboard'とラベル付けされ、値が'false'のisPlayerAという状態バブルを持っています。状態バブルは黄色でハイライトされており、変更されたことを示しています。左側の子要素は削除されたことを示す黄色の'poof'画像に置き換えられており、右側には追加されたことを示す黄色でハイライトされた新しい子要素があります。新しい子要素は'Counter'とラベル付けされ、値0の'count'というラベルの状態バブルを含んでいます。Reactコンポーネントのツリー図。親は'Scoreboard'とラベル付けされ、値が'false'のisPlayerAという状態バブルを持っています。状態バブルは黄色でハイライトされており、変更されたことを示しています。左側の子要素は削除されたことを示す黄色の'poof'画像に置き換えられており、右側には追加されたことを示す黄色でハイライトされた新しい子要素があります。新しい子要素は'Counter'とラベル付けされ、値0の'count'というラベルの状態バブルを含んでいます。

「next」をクリック

Reactコンポーネントのツリー図。親は'Scoreboard'とラベル付けされ、値が'true'のisPlayerAという状態バブルを持っています。状態バブルは黄色でハイライトされており、変更されたことを示しています。左側には追加されたことを示す黄色でハイライトされた新しい子要素があります。新しい子要素は'Counter'とラベル付けされ、値0の'count'というラベルの状態バブルを含んでいます。右側の子要素は削除されたことを示す黄色の'poof'画像に置き換えられています。Reactコンポーネントのツリー図。親は'Scoreboard'とラベル付けされ、値が'true'のisPlayerAという状態バブルを持っています。状態バブルは黄色でハイライトされており、変更されたことを示しています。左側には追加されたことを示す黄色でハイライトされた新しい子要素があります。新しい子要素は'Counter'とラベル付けされ、値0の'count'というラベルの状態バブルを含んでいます。右側の子要素は削除されたことを示す黄色の'poof'画像に置き換えられています。

再度「next」をクリック

Counterの状態は、DOMから削除されるたびに破棄されます。これが、ボタンをクリックするたびにリセットされる理由です。

この解決策は、同じ場所にレンダリングされる独立したコンポーネントが少数しかない場合に便利です。この例では2つしかないため、JSXでそれぞれを別々にレンダリングするのは手間ではありません。

オプション2: keyを使用して状態をリセットする

コンポーネントの状態をリセットする、もう1つのより一般的な方法もあります。

リストをレンダリングする際にkeyを見たことがあるかもしれません最初のカウンターや2番目のカウンターではなく、特定のカウンター、例えばTaylorのカウンターであることをReactに伝えることができます。これにより、Reactはツリー内のどこに現れてもTaylorのカウンターを認識します!

この例では、2つの<Counter />は、JSX内で同じ場所に現れていても状態を共有しません:

TaylorとSarahを切り替えても状態は保持されません。これは、異なるkeyを指定したためです:

削除されるたびに、その状態が破棄されます

注意

keyはグローバルに一意である必要はないことを覚えておいてください。keyは、親内での位置を指定するだけです。

keyを使用してフォームをリセットする

keyを使用した状態のリセットは、フォームを扱う際に特に有用です。

このチャットアプリでは、<Chat>コンポーネントがテキスト入力の状態を含んでいます:

入力欄に何か入力してから、「Alice」または「Bob」を押して別の受信者を選択してみてください。ツリー内の同じ位置に<Chat>がレンダリングされるため、入力状態が保持されていることに気づくでしょう。

多くのアプリでは、これは望ましい動作かもしれませんが、チャットアプリではそうではありません!誤ってクリックしたために、ユーザーが既に入力したメッセージを間違った相手に送信することを許したくはないでしょう。これを修正するには、keyを追加します:

これにより、別の受信者を選択したときに、Chatコンポーネントがその下のツリー内の状態も含めて一から再作成されることが保証されます。ReactはDOM要素を再利用する代わりに再作成します。

これで、受信者を切り替えると常にテキストフィールドがクリアされます:

Deep Dive
削除されたコンポーネントの状態を保持する

まとめ

  • Reactは、同じコンポーネントが同じ位置にレンダリングされている限り、その状態を保持します。
  • 状態はJSXタグ内には保持されません。そのJSXを配置したツリー内の位置に関連付けられています。
  • サブツリーの状態をリセットするには、異なるkeyを与えることで強制できます。
  • コンポーネント定義をネストしないでください。そうしないと、誤って状態がリセットされてしまいます。

Try out some challenges

Challenge 1 of 5:Fix disappearing input text #

This example shows a message when you press the button. However, pressing the button also accidentally resets the input. Why does this happen? Fix it so that pressing the button does not reset the input text.