stateのリフトアップとは、共有する必要のあるコンポーネントからstateを取り出し、最も近い共通の親に移すことです。親が値を持ってpropとして下へ渡し、子が呼んで値を変えるための関数も渡します。こうしてすべての子が、1つのstateのコピーから描画されます。
どれかのパネルで「Show」をクリックしてください。そのパネルが開き、開いていたパネルは閉じます。一度に開けるパネルが1つだけなのは、パネルが自分でそれを決めていないからです。App が openIndex を持ち、各 Panel は isOpen と onOpen 関数を受け取るだけです。
問題:一致すべきstate
わかりやすい版、つまり各パネルが自分の isOpen stateを持つ版から始めましょう。どのパネルも動きますが、パネルはお互いのことを何も知らないので、2つが同時に開くのを止めるものがありません。
両方のパネルを開いてください。両方とも開いたままになります。Reactのstateはそれを宣言したコンポーネントに閉じているので、兄弟はそれを読むこともリセットすることもできません。2つのコンポーネントが一致する必要があるなら、stateは両方より上に置かなければなりません。
3ステップでstateをリフトアップする
2つ目の例を1つ目の例に変えるには、3つの編集が必要です。
- 子からstateを取り除く。
PanelのuseStateを削除し、代わりにpropsからisOpenを読みます。子はもう、自分が開いているかどうかを決めません。 - 親から値と、それを変える手段を渡す。
PanelはisOpenとonOpenコールバックを受け取ります。子はボタンがクリックされるとonOpen()を呼びますが、親がそれで何をするかは知りません。 - 共通の親にstateを追加する。
AppがopenIndexを宣言し、それを各パネルのpropsに変えます:isOpen={openIndex === 1}とonOpen={() => setOpenIndex(1)}。
最も近い共通の親とは、stateを必要とするすべてのコンポーネントを描画する、一番下のコンポーネントです。ここでは App です。パネルが Faq コンポーネントの中にあれば、stateはそれより上ではなく Faq に置きます。
リフトアップの後、Panel は制御された入力と同じ意味で親に制御されています。propsが示すものを表示し、変更をコールバックで知らせます。フォーム要素についての同じ考え方は、制御コンポーネントと非制御コンポーネントのページで扱っています。
信頼できる唯一の情報源
画面の2か所が同じ事実を表示するなら、その事実は1回だけ保存し、ほかはすべてそこから計算します。典型的な例は温度の換算です。摂氏と華氏の入力欄は常に一致しなければならないので、それぞれが自分の数値を持つわけにはいきません。
どちらかの欄に入力すると、もう一方がそれに続きます。stateは1つの事実、つまりユーザーが最後に入力した数値と、それがどちらの単位だったかです。もう一方の欄はレンダリング中にそこから計算されるので、入力中の欄は入力したものをそのまま正確に保ちます。初期stateを { value: '212', scale: 'f' } に変えると、入力欄の下のメッセージが「Water boils.」に切り替わります。
摂氏の数値と華氏の数値を2つのstateに持つと、すべてのハンドラが両方を更新しなければならず、1つのハンドラが更新を忘れた瞬間に2つの入力欄が食い違います。保存された1つの値が、自分自身と食い違うことはありません。
セッターを下へ渡す
子が親のstateを変えられるのは、親が渡した関数を通じてだけです。セッターそのもの(onSelect={setColor})を渡すことも、もっと多くのことをする関数(onOpen={() => setOpenIndex(1)})を渡すこともできます。propには setSelected ではなく onSelect や onChange のようなイベント風の名前を付けると、子は親が値をどう保存しているかを知らずに済むので、後で親がそれを変えても子に手を入れる必要がありません。
ColorPicker と Preview はお互いにやり取りしません。ピッカーが選択を上へ知らせ、App がそれを保存し、新しい値が両方へ流れていきます。
stateをリフトアップすべきでないとき
リフトアップにはコストがあります。stateが親にあると、変更のたびに親が再レンダリングされ、デフォルトではそのstateを使わない子も含めてすべての子が再レンダリングされます。持ち上げるのは最も近い共通の親までにし、1つのコンポーネントしか使わないstateはそのコンポーネントの中に残してください。
入力欄に入力してコンソールを見てください。キーを押すたびにレンダリングされるのは SearchBox だけです。次に、query を App に移し、propsとして SearchBox に渡してみてください。リストは検索語を使っていないのに、キーを押すたびに ProductList も記録されるようになります。リストが検索語でフィルターしているなら、両方のコンポーネントが同じ値に依存するので、リフトアップが正しい判断です。
問うべきなのは「この値を読む必要があるのは誰か?」です。答えが1つのコンポーネントなら、stateはそこに残します。複数なら、それらの最も近い共通の親に移します。
リフトアップが行き過ぎたとき
最も近い共通の親がツリーのずっと上にあり、値を受け渡すだけのコンポーネントをいくつも通して渡さなければならないことがあります。これがpropsのバケツリレーです。数層のpropsなら問題なく、追うのも簡単です。同じ値が多くの層を通る場合や、ほとんどすべてのコンポーネントがそれを必要とする場合(ログイン中のユーザー、テーマ、言語)は、手で渡す代わりにuseContextで読んでください。コンテキストが変えるのは値が子に届く方法であって、state自体は相変わらず1つの親にあるので、リフトアップされていることに変わりはありません。
よくある質問
Reactのstateのリフトアップとは何ですか?
あるstateを、それを使うコンポーネントから、最も近い共通の親に移すことです。親がstateを持ち、値とそれを変える関数をpropsとして子に渡します。
Reactで2つの兄弟コンポーネントがstateを共有するには?
兄弟はお互いのstateを読めません。stateを共通の親に置き、値を両方に渡し、それを変える側にはセッター(または onChange のようなハンドラ)を渡します。そうすれば両方の兄弟が同じ値から描画されます。
子コンポーネントが親のstateを更新するには?
親が、onSelect={setSelected} や onSelect={(id) => setSelected(id)} のように関数をpropとして渡し、子がそれを呼びます。stateは親に残り、子は変更を頼むだけです。
stateをリフトアップすべきでないのはどんなときですか?
1つのコンポーネントしかそのstateを使わないときです。必要以上に上へ持ち上げると、変更のたびに親がレンダリングされ、関係のないコンポーネントにまでpropsが広がります。stateはできるだけ使う場所の近くに置いてください。
stateを上げすぎる代わりにどうすればよいですか?
同じpropsを何層も通して渡しているなら、代わりにコンテキスト(useContext)で共有の値を読むか、それを必要とするコンポーネントが近くに来るように構造を変えてください。