状態の保持とリセット
状態はコンポーネント間で独立しています。Reactは、UIツリー内の位置に基づいて、どの状態がどのコンポーネントに属するかを追跡します。再レンダリング間で状態をいつ保持し、いつリセットするかを制御できます。
学習内容
- Reactが状態を保持またはリセットするタイミング
- コンポーネントの状態を強制的にリセットする方法
- キーとタイプが状態の保持に与える影響
状態はレンダーツリー内の位置に結びついている
Reactは、UI内のコンポーネント構造に対してレンダーツリーを構築します。
コンポーネントに状態を与えると、状態がコンポーネントの「内部」に「存在」すると考えるかもしれません。しかし、状態は実際にはReact内部で保持されています。Reactは、保持している各状態の断片を、そのコンポーネントがレンダーツリー内のどこに位置するかによって、正しいコンポーネントに関連付けます。
ここでは、<Counter />というJSXタグは1つだけですが、2つの異なる位置でレンダリングされています:
これらをツリーとして表すと次のようになります:


Reactツリー
これらはツリー内のそれぞれ独自の位置でレンダリングされるため、2つの独立したカウンターです。Reactを使用する際にこれらの位置について考える必要は通常ありませんが、その仕組みを理解しておくと役立つことがあります。
Reactでは、画面上の各コンポーネントは完全に独立した状態を持ちます。例えば、2つのCounterコンポーネントを並べてレンダリングすると、それぞれが独自の独立したscore状態とhover状態を持ちます。
両方のカウンターをクリックしてみて、互いに影響を与えないことを確認してください:
ご覧の通り、一方のカウンターが更新されると、そのコンポーネントの状態のみが更新されます:


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


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


コンポーネントの追加
Reactは、コンポーネントがUIツリー内のその位置でレンダリングされている限り、その状態を保持します。コンポーネントが削除されるか、同じ位置に別のコンポーネントがレンダリングされると、Reactはその状態を破棄します。
同じ位置にある同じコンポーネントは状態を保持する
この例では、2つの異なる<Counter />タグがあります:
チェックボックスをオンまたはオフにしても、カウンターの状態はリセットされません。isFancyがtrueであろうとfalseであろうと、ルートのAppコンポーネントから返されるdivの最初の子として、常に<Counter />があります:


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を削除し、その状態を破棄しました。


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


戻すと、pが削除され、Counterが追加されます
また、同じ位置で異なるコンポーネントをレンダリングすると、そのサブツリー全体の状態がリセットされます。これがどのように機能するかを確認するには、カウンターを増やしてからチェックボックスをオンにしてください:
チェックボックスをクリックすると、カウンターの状態がリセットされます。Counterをレンダリングしていますが、divの最初の子要素がsectionからdivに変わります。子要素のsectionがDOMから削除されると、その下のツリー全体(Counterとその状態を含む)も破棄されます。


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


戻り切り替え時、divが削除され、新しいsectionが追加されます。
経験則として、再レンダリング間で状態を保持したい場合、ツリーの構造がレンダリング間で「一致」する必要があります。構造が異なる場合、Reactはツリーからコンポーネントを削除する際に状態を破棄するため、状態は失われます。
落とし穴
これが、コンポーネント関数の定義をネストすべきではない理由です。
ここでは、MyTextFieldコンポーネント関数が内部MyComponentで定義されています:
ボタンをクリックするたびに、入力状態が消えます!これは、異なるMyTextField関数がMyComponentのレンダリングごとに作成されるためです。同じ位置に異なるコンポーネントをレンダリングしているため、Reactはその下のすべての状態をリセットします。これはバグやパフォーマンス問題を引き起こします。この問題を避けるには、コンポーネント関数は常にトップレベルで宣言し、定義をネストしないでください。
同じ位置での状態のリセット
デフォルトでは、Reactはコンポーネントが同じ位置にある間、その状態を保持します。通常、これはまさに望ましい動作であり、デフォルトの動作として理にかなっています。しかし、時にはコンポーネントの状態をリセットしたい場合があります。各ターン中に2人のプレイヤーがスコアを記録できるこのアプリを考えてみましょう:
現在、プレイヤーを変更してもスコアは保持されます。2つのCounterは同じ位置に表示されるため、Reactはそれらを同じCounterであり、personプロップが変更されたものと見なします。
しかし概念的には、このアプリではこれらは2つの独立したカウンターであるべきです。UI上では同じ場所に表示されるかもしれませんが、一方はTaylor用のカウンター、もう一方はSarah用のカウンターです。
これらを切り替える際に状態をリセットする方法は2つあります:
- コンポーネントを異なる位置でレンダリングする
- 各コンポーネントに
keyで明示的な識別子を与える
オプション1:コンポーネントを異なる位置でレンダリングする
これら2つのCounterを独立させたい場合は、2つの異なる位置でレンダリングできます:
- 最初、
isPlayerAはtrueです。したがって、最初の位置にはCounterの状態が含まれ、2番目の位置は空です。 - 「Next player」ボタンをクリックすると、最初の位置はクリアされますが、2番目の位置に
Counterが含まれるようになります。


初期状態


「next」をクリック


再度「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要素を再利用する代わりに再作成します。
これで、受信者を切り替えると常にテキストフィールドがクリアされます:
まとめ
- 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.
