コンポーネント間での状態の共有

場合によっては、2つのコンポーネントの状態を常に一緒に変更したいことがあります。そのためには、両方のコンポーネントから状態を削除し、それらを最も近い共通の親コンポーネントに移動し、props を介してそれらに渡します。これは状態の持ち上げと呼ばれ、React コードを書く際に最も一般的な操作の1つです。

学習内容

  • 状態を上げてコンポーネント間で共有する方法
  • 制御されたコンポーネントと制御されていないコンポーネントとは何か

例による状態の持ち上げ

この例では、親の Accordion コンポーネントが2つの別々の Panel をレンダリングします。

  • アコーディオン
    • パネル
    • パネル

それぞれの Panel コンポーネントは、コンテンツが表示されるかどうかを決定するブール値の isActive 状態を持っています。

両方のパネルの「表示」ボタンを押してください

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>
    </>
  );
}

一方のパネルのボタンを押しても、もう一方のパネルには影響がないことに注意してください。— それらは独立しています。

Diagram showing a tree of three components, one parent labeled Accordion and two children labeled Panel. Both Panel components contain isActive with value false.
Diagram showing a tree of three components, one parent labeled Accordion and two children labeled Panel. Both Panel components contain isActive with value false.

最初は、それぞれの PanelisActive 状態は false なので、どちらも折りたたまれた状態で表示されます。

The same diagram as the previous, with the isActive of the first child Panel component highlighted indicating a click with the isActive value set to true. The second Panel component still contains value false.
The same diagram as the previous, with the isActive of the first child Panel component highlighted indicating a click with the isActive value set to true. The second Panel component still contains value false.

いずれかの Panel のボタンをクリックすると、その PanelisActive 状態のみが更新されます。

しかし、今度はいつでも1つのパネルだけを展開するように変更したいとします。 その設計では、2番目のパネルを展開すると、最初のパネルが折りたたまれる必要があります。どうすればいいでしょうか?

これらの2つのパネルを調整するには、3つの手順で状態を親コンポーネントに「持ち上げる」必要があります。

  1. 子コンポーネントから状態を削除します。
  2. 共通の親からハードコードされたデータを渡します。
  3. 共通の親に状態を追加し、イベントハンドラと一緒に渡します。

これにより、Accordion コンポーネントは両方の Panel を調整し、一度に1つだけ展開することができます。

手順 1: 子コンポーネントから状態を削除する

PanelisActive の制御を親コンポーネントに渡します。これは、親コンポーネントが isActive をpropとして Panel に渡すことを意味します。Panel コンポーネントから この行を削除することから始めます。

const [isActive, setIsActive] = useState(false);

そして、代わりに isActivePanel の props のリストに追加します。

function Panel({ title, children, isActive }) {

これで、Panel の親コンポーネントは、propとして渡すことで isActive制御できるようになりました。逆に、Panel コンポーネントは isActive の値を制御できなくなりました。— 親コンポーネント次第です!

手順 2: 共通の親からハードコードされたデータを渡す

状態を上げるには、調整したい両方の子コンポーネントの最も近い共通の親コンポーネントを見つける必要があります。

  • Accordion (最も近い共通の親)
    • パネル
    • パネル

この例では、Accordion コンポーネントが該当します。これは両方のパネルの上に位置し、それらのプロパティを制御できるため、現在アクティブなパネルの「真実の源」となります。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: 共通の親に状態を追加する

状態を上に持ち上げることで、状態として保存しているものの性質が変わる事がよくあります。

この場合、一度にアクティブにできるパネルは1つだけです。これは、Accordion の共通の親コンポーネントが、*どの* パネルがアクティブであるかを追跡する必要があることを意味します。ブール値の代わりに、アクティブな Panel のインデックスとして数値を状態変数に使用できます。

const [activeIndex, setActiveIndex] = useState(0);

activeIndex0 の場合、最初のパネルがアクティブになり、1 の場合は、2番目のパネルがアクティブになります。

いずれかの Panel の「表示」ボタンをクリックすると、Accordion のアクティブなインデックスが変更されます。PanelAccordion 内で定義されているため、activeIndex 状態を直接設定できません。Accordion コンポーネントは、イベントハンドラーをプロパティとして渡すことで、Panel コンポーネントがその状態を変更することを*明示的に許可*する必要があります。

<>
<Panel
isActive={activeIndex === 0}
onShow={() => setActiveIndex(0)}
>
...
</Panel>
<Panel
isActive={activeIndex === 1}
onShow={() => setActiveIndex(1)}
>
...
</Panel>
</>

Panel 内の <button> は、クリックイベントハンドラーとして 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>
  );
}

これで状態の持ち上げは完了です!状態を共通の親コンポーネントに移動することで、2つのパネルを連携させることができました。「表示されているか」を示す2つのフラグの代わりにアクティブなインデックスを使用することで、一度に1つのパネルのみがアクティブになることが保証されます。また、イベントハンドラーを子に渡すことで、子が親の状態を変更できるようになりました。

Diagram showing a tree of three components, one parent labeled Accordion and two children labeled Panel. Accordion contains an activeIndex value of zero which turns into isActive value of true passed to the first Panel, and isActive value of false passed to the second Panel.
Diagram showing a tree of three components, one parent labeled Accordion and two children labeled Panel. Accordion contains an activeIndex value of zero which turns into isActive value of true passed to the first Panel, and isActive value of false passed to the second Panel.

初期状態では、AccordionactiveIndex0 なので、最初の PanelisActive = true を受け取ります。

The same diagram as the previous, with the activeIndex value of the parent Accordion component highlighted indicating a click with the value changed to one. The flow to both of the children Panel components is also highlighted, and the isActive value passed to each child is set to the opposite: false for the first Panel and true for the second one.
The same diagram as the previous, with the activeIndex value of the parent Accordion component highlighted indicating a click with the value changed to one. The flow to both of the children Panel components is also highlighted, and the isActive value passed to each child is set to the opposite: false for the first Panel and true for the second one.

AccordionactiveIndex 状態が 1 に変わると、2番目の Panel は代わりに isActive = true を受け取ります。

詳細

制御されたコンポーネントと制御されていないコンポーネント

ローカル状態を持つコンポーネントを「制御されていない」と呼ぶことはよくあります。たとえば、isActive 状態変数を持つ元の Panel コンポーネントは、親がパネルがアクティブであるかどうかを制御できないため、制御されていません。

対照的に、コンポーネント内の重要な情報が、独自のローカル状態ではなくプロパティによって駆動される場合、そのコンポーネントは「制御されている」と言うことができます。これにより、親コンポーネントはその動作を完全に指定できます。isActive プロパティを持つ最終的な Panel コンポーネントは、Accordion コンポーネントによって制御されます。

制御されていないコンポーネントは、設定が少なくて済むため、親の中で使いやすくなっています。ただし、それらをまとめて調整する場合、柔軟性が低くなります。制御されたコンポーネントは最大限の柔軟性を備えていますが、親コンポーネントがプロパティを使用して完全に設定する必要があります。

実際には、「制御された」と「制御されていない」は厳密な技術用語ではなく、各コンポーネントには通常、ローカル状態とプロパティの両方が混在しています。ただし、これは、コンポーネントの設計方法と提供される機能について説明するのに便利な方法です。

コンポーネントを作成するときは、その中のどの情報を制御する必要があるか(プロパティ経由)、どの情報を制御しないでおく必要があるか(状態経由)を検討してください。ただし、いつでも考えを変えて、後でリファクタリングできます。

各状態の単一の真実の源

Reactアプリケーションでは、多くのコンポーネントが独自のステートを持ちます。入力欄のように、一部のステートはツリーの最下部にあるリーフコンポーネントの近くに「存在」する場合があります。他のステートは、アプリの上部に「存在」する場合があります。たとえば、クライアントサイドのルーティングライブラリでさえ、通常は現在のルートをReactステートに格納し、propsによってそれを下位に渡すことで実装されます!

それぞれの固有のステートについて、それを「所有」するコンポーネントを選択します。この原則は、「単一の情報源」(“Single Source of Truth”)を持つこととしても知られています。これは、すべてのステートが1か所に存在することを意味するのではなく、*各*ステートについて、その情報を保持する*特定の*コンポーネントがあることを意味します。コンポーネント間で共有ステートを複製する代わりに、共通の親コンポーネントに*持ち上げて*、それを必要とする子コンポーネントに*渡します*。

アプリは作業を進めるにつれて変化します。それぞれのステートがどこに「存在」するのかをまだ理解している段階では、ステートを下位または上位に移動することはよくあることです。これはすべてプロセスの一部です!

もう少し多くのコンポーネントでこれが実際にどのように感じられるかを確認するには、Thinking in Reactをご覧ください。

要約(リンクアイコン)

  • 2つのコンポーネントを連携させたい場合は、それらのステートを共通の親コンポーネントに移動します。
  • 次に、共通の親コンポーネントからpropsを介して情報を下位に渡します。
  • 最後に、子コンポーネントが親コンポーネントのステートを変更できるように、イベントハンドラを下位に渡します。
  • コンポーネントを「制御された」(propsによって駆動される)または「制御されていない」(ステートによって駆動される)と考えることは役立ちます。

チャレンジ 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>
  );
}