コンポーネント間で state を共有する
2 つのコンポーネントの state を常に同時に変更したいという場合があります。これを行うには、両方のコンポーネントから state を削除して最も近い共通の親へ移動し、そこから state を props 経由でコンポーネントへ渡します。これは state のリフトアップ (lifting state up) として知られているものであり、React コードを書く際に行う最も一般的な作業のひとつです。
このページで学ぶこと
- コンポーネント間で state を共有する方法
- 制御された (controlled) コンポーネントと非制御 (uncontrolled) コンポーネントとは何か
state のリフトアップの例
以下の例では、親の Accordion
コンポーネントが 2 つの別々の Panel
をレンダーしています。
Accordion
Panel
Panel
各 Panel
コンポーネントには、内容を表示中かどうかを決定するブール型の isActive
という state があります。
両方のパネルで “Show” ボタンを押してみてください。
import { useState } from 'react'; function Panel({ title, children }) { const [isActive, setIsActive] = useState(false); return ( <section className="panel"> <h3>{title}</h3> {isActive ? ( <p>{children}</p> ) : ( <button onClick={() => setIsActive(true)}> Show </button> )} </section> ); } export default function Accordion() { return ( <> <h2>Almaty, Kazakhstan</h2> <Panel title="About"> With a population of about 2 million, Almaty is Kazakhstan's largest city. From 1929 to 1997, it was its capital city. </Panel> <Panel title="Etymology"> The name comes from <span lang="kk-KZ">алма</span>, the Kazakh word for "apple" and is often translated as "full of apples". In fact, the region surrounding Almaty is thought to be the ancestral home of the apple, and the wild <i lang="la">Malus sieversii</i> is considered a likely candidate for the ancestor of the modern domestic apple. </Panel> </> ); }
片方のパネルのボタンを押しても、もう片方のパネルには影響しません。2 つのパネルは独立していますね。
ですが今回はこれを変更し、一度に 1 つのパネルだけが展開されるようにしたいとしましょう。この設計では、2 番目のパネルを展開すると 1 番目のパネルが折りたたまれます。どのようにして実現すればよいでしょうか?
これら 2 つのパネルを協調して動作させるためには、以下の 3 ステップで、親のコンポーネントに “state をリフトアップ” する必要があります。
- 子コンポーネントから state を削除する。
- 共通の親からハードコードされたデータを渡す。
- 共通の親に state を追加し、イベントハンドラと一緒に下に渡す。
これにより、Accordion
コンポーネントが両方の Panel
の調整役となり、一度に一方だけを展開できるようになります。
ステップ 1:子コンポーネントから state を削除する
Panel
の isActive
の制御権を親コンポーネントに与えることになります。つまり、親コンポーネントが isActive
を Panel
に props として渡すということです。まずは Panel
コンポーネントから以下の行を削除してください。
const [isActive, setIsActive] = useState(false);
代わりに、isActive
を Panel
の props のリストに追加します。
function Panel({ title, children, isActive }) {
これで、Panel
の親コンポーネントは isActive
を props として渡すことで制御できます。逆に、Panel
コンポーネントは isActive
の値を自身で制御できなくなりました。制御が親コンポーネントに移ったのです!
ステップ 2:共通の親からハードコードされたデータを渡す
state をリフトアップするためには、協調動作させたいすべての子コンポーネントの、最も近い共通の親コンポーネントを特定する必要があります。
Accordion
(最も近い共通の親)Panel
Panel
この例では Accordion
コンポーネントが該当します。両方のパネルの上にあり、それらの props を制御できるので、現在アクティブなパネルに関する “信頼できる情報源 (source of truth)” となります。Accordion
コンポーネントからハードコードされた isActive
の値(例えば、true
)を両方のパネルに渡しましょう。
import { useState } from 'react'; export default function Accordion() { return ( <> <h2>Almaty, Kazakhstan</h2> <Panel title="About" isActive={true}> With a population of about 2 million, Almaty is Kazakhstan's largest city. From 1929 to 1997, it was its capital city. </Panel> <Panel title="Etymology" isActive={true}> The name comes from <span lang="kk-KZ">алма</span>, the Kazakh word for "apple" and is often translated as "full of apples". In fact, the region surrounding Almaty is thought to be the ancestral home of the apple, and the wild <i lang="la">Malus sieversii</i> is considered a likely candidate for the ancestor of the modern domestic apple. </Panel> </> ); } function Panel({ title, children, isActive }) { return ( <section className="panel"> <h3>{title}</h3> {isActive ? ( <p>{children}</p> ) : ( <button onClick={() => setIsActive(true)}> Show </button> )} </section> ); }
Accordion
コンポーネントにハードコードされた isActive
の値を編集してみて、画面上で起きる結果を確認してください。
ステップ 3:共通の親に state を追加する
state をリフトアップすることで、state として格納するデータの意味が変わることがあります。
今回の場合、一度に 1 つのパネルだけがアクティブであるべきです。つまり、共通の親コンポーネントである Accordion
は、どのパネルがアクティブなのかを管理する必要があります。state 変数としては、boolean
値の代わりに、アクティブな Panel
のインデックスを表す数値を使うことができます。
const [activeIndex, setActiveIndex] = useState(0);
activeIndex
が 0
のときは 1 番目のパネルが、1
のときは 2 番目のパネルがアクティブになります。
どちらの Panel
の “Show” ボタンがクリックされた場合でも、Accordion
のアクティブインデックスを変更する必要があります。activeIndex
という state は Accordion
内に定義されるものであるため、Panel
からそれを直接セットすることはできません。Accordion
コンポーネントは、props として onShow
イベントハンドラを下に渡すことで、Panel
コンポーネントがアコーディオンの state を変更できるように明示的に許可する必要があります:
<>
<Panel
isActive={activeIndex === 0}
onShow={() => setActiveIndex(0)}
>
...
</Panel>
<Panel
isActive={activeIndex === 1}
onShow={() => setActiveIndex(1)}
>
...
</Panel>
</>
そして Panel
内の <button>
は、クリックイベントハンドラとして props である onShow
を使用します。
import { useState } from 'react'; export default function Accordion() { const [activeIndex, setActiveIndex] = useState(0); return ( <> <h2>Almaty, Kazakhstan</h2> <Panel title="About" isActive={activeIndex === 0} onShow={() => setActiveIndex(0)} > With a population of about 2 million, Almaty is Kazakhstan's largest city. From 1929 to 1997, it was its capital city. </Panel> <Panel title="Etymology" isActive={activeIndex === 1} onShow={() => setActiveIndex(1)} > The name comes from <span lang="kk-KZ">алма</span>, the Kazakh word for "apple" and is often translated as "full of apples". In fact, the region surrounding Almaty is thought to be the ancestral home of the apple, and the wild <i lang="la">Malus sieversii</i> is considered a likely candidate for the ancestor of the modern domestic apple. </Panel> </> ); } function Panel({ title, children, isActive, onShow }) { return ( <section className="panel"> <h3>{title}</h3> {isActive ? ( <p>{children}</p> ) : ( <button onClick={onShow}> Show </button> )} </section> ); }
これで state のリフトアップが完了です! state を共通の親コンポーネントに移動させることで、2 つのパネルを協調動作させられるようになりました。「表示中」フラグを 2 つ使う代わりにアクティブインデックスを使用することで、一度にアクティブなパネルが 1 つだけであることが保証されました。また、イベントハンドラを子に渡すことで、子に親の state を変更させることができました。
さらに深く知る
一般的に、ローカル state を持つコンポーネントを “非制御 (uncontrolled)” であると呼びます。例えば、isActive
という state 変数を持つ元の Panel
コンポーネントは、パネルがアクティブかどうかに関して親が影響を与えることができないため、非制御コンポーネントです。
対照的に、重要な情報がローカル state ではなく props によって駆動されるとき、コンポーネントは “制御された (controlled)” ものと呼ばれることがあります。これにより、親コンポーネントがその振る舞いを完全に指定することができます。isActive
を props として持つ最終的な Panel
コンポーネントは、Accordion
コンポーネントによって制御されていることになります。
非制御コンポーネントは、設定が少なくて済むので親コンポーネントの中に入れて使用することが簡単にできます。しかし、それらを協調動作させたい場合に柔軟性がありません。制御されたコンポーネントはとても柔軟ですが、親コンポーネントが props で完全に設定してあげる必要があります。
実際には、“制御された”、“非制御” は技術用語として厳密なものではありません。各コンポーネントは通常、ローカルな state と props の両方を、混在して持つものです。しかし、コンポーネントがどう設計されるか、どんな機能を持つかについて話す際には、このような考え方が役に立つでしょう。
コンポーネントを書くときには、その中のどの情報を(props で)制御し、どの情報を(state を使うことで)制御しないのかを検討してください。しかし後で考えを変えてリファクタリングすることはいつでも可能です。
各 state の信頼できる唯一の情報源
React アプリケーションでは、多くのコンポーネントが自身の state を保持します。一部の state は、入力フィールドのような末梢コンポーネント(ツリーの最下部のコンポーネント)に近いところに存在します。一部の state はアプリの上部に近いところに存在することでしょう。例えば、クライアントサイドルーティングライブラリも、React の state に現在のルートを格納し、props を介して下に渡すことで実装されることが一般的です。
それぞれの state について、それを「所有」するコンポーネントを選択してください。この原則は、“信頼できる唯一の情報源 (single source of truth)” としても知られています。これは、すべての state が一箇所にまとまっているという意味ではありません。それぞれの state について、その情報を保持する特定のコンポーネントが存在すべきという意味です。コンポーネント間で共有される state は複製する代わりに、共通の親にリフトアップして、それを必要とする子に渡すようにしてください。
あなたのアプリは作業を進めるうちに変化していきます。まだそれぞれの state がどこに存在すべきか分からない間は、state を下に移動させたり、上に戻したりすることがよくあります。これは開発プロセスの一環です!
もう少し多くのコンポーネントが登場する例で実践的に感覚を理解したい場合は、React の流儀を読んでみましょう。
まとめ
- 2 つのコンポーネントを協調動作させたい場合は、state を共通の親に移動する。
- 次に、その共通の親から props 経由で情報を下に渡す。
- 最後に、子が親の state を変更できるよう、イベントハンドラを下に渡す。
- コンポーネントを「制御された」(props によって駆動される)か「非制御」(state によって駆動される)かという観点で考えることが有用である。
チャレンジ 1/2: 入力欄の同期
以下の 2 つの入力欄は独立しています。同期して動作するようにしましょう。片方の入力欄を編集すると、他方の入力欄も同じテキストに更新されるようにしてください。
import { useState } from 'react'; export default function SyncedInputs() { return ( <> <Input label="First input" /> <Input label="Second input" /> </> ); } function Input({ label }) { const [text, setText] = useState(''); function handleChange(e) { setText(e.target.value); } return ( <label> {label} {' '} <input value={text} onChange={handleChange} /> </label> ); }