コンポーネント間で 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 つのパネルは独立していますね。

Accordion というラベルが付いた親と、Panel というラベルが付いた 2 つの子からなる 3 つのコンポーネントのツリーを示す図。両方の Panel コンポーネントには、false の値を持った isActive が含まれている。
Accordion というラベルが付いた親と、Panel というラベルが付いた 2 つの子からなる 3 つのコンポーネントのツリーを示す図。両方の Panel コンポーネントには、false の値を持った isActive が含まれている。

最初は、各 PanelisActive state は false なので、どちらも折りたたまれている

前の図と同様で、最初の子の Panel コンポーネントの isActive がクリックによりハイライトされ、値が true に設定されている。2 つ目の Panel コンポーネントは false のまま。
前の図と同様で、最初の子の Panel コンポーネントの isActive がクリックによりハイライトされ、値が true に設定されている。2 つ目の Panel コンポーネントは false のまま。

どちらかの Panel のボタンをクリックすると、その Panel のみ isActive の state が更新される

ですが今回はこれを変更し、一度に 1 つのパネルだけが展開されるようにしたいとしましょう。この設計では、2 番目のパネルを展開すると 1 番目のパネルが折りたたまれます。どのようにして実現すればよいでしょうか?

これら 2 つのパネルを協調して動作させるためには、以下の 3 ステップで、親のコンポーネントに “state をリフトアップ” する必要があります。

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

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

ステップ 1:子コンポーネントから state を削除する

PanelisActive の制御権を親コンポーネントに与えることになります。つまり、親コンポーネントが isActivePanel に props として渡すということです。まずは Panel コンポーネントから以下の行を削除してください。

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

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

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

これで、Panel の親コンポーネントは isActiveprops として渡すことで制御できます。逆に、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);

activeIndex0 のときは 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 を変更させることができました。

Accordion というラベルの親コンポーネントと、Panel というラベルの 2 つの子コンポーネントからなる 3 つのコンポーネントのツリーを示す図。最初 Accordion の activeIndex が 0 なので、最初の Panel が isActive = true を受け取る。
Accordion というラベルの親コンポーネントと、Panel というラベルの 2 つの子コンポーネントからなる 3 つのコンポーネントのツリーを示す図。最初 Accordion の activeIndex が 0 なので、最初の Panel が isActive = true を受け取る。

最初、AccordionactiveIndex0 なので、最初の PanelisActive = true を受け取る。

前と同じ図だが、クリックによりハイライトされた親の Accordion コンポーネントの activeIndex が 1 に変わっている。両方の子の Panel コンポーネントもハイライトされており、isActive の値が逆転して渡されている。最初の Panel には false、2 番目の Panel には true。
前と同じ図だが、クリックによりハイライトされた親の Accordion コンポーネントの activeIndex が 1 に変わっている。両方の子の Panel コンポーネントもハイライトされており、isActive の値が逆転して渡されている。最初の Panel には false、2 番目の Panel には true。

AccordionactiveIndex state が 1 に変更されると、2 番目の PanelisActive = true を受け取る。

さらに深く知る

制御されたコンポーネントと非制御コンポーネント

一般的に、ローカル 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>
  );
}