React Docs BETAを読む 【コンポーネント間で状態を共有する】

2022/07/08に公開

React Docs BETAを読む 【状態の管理】
React Docs BETAを読む 【Stateで入力に反応する】
React Docs BETAを読む 【状態構造の選択】
に引き続き状態管理の章を読み進めていきます。
本日はコンポーネント間で状態を共有するという章です。

Sharing State Between Components

コンポーネントの実装をしていくと、コンポーネント間で状態を同期したい場合などが出てくると思います。
フォームの入力であったり、パネルのオープンなどいろいろ思いつきますね。
状態を常に一緒に変化させるためには、子で持っている状態を親に移動してpropsとして渡せばよいです。
この方法は公式ドキュメントにも、実際のソースにもよく出てきます。
これを状態の引き上げと呼びます。

Lifting state up by example

この節では状態の引き上げを以下の例を交えて解説しています。

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

この例ではパネルのオープンを同期したいという例ですね。
それぞれの子コンポーネントがisActiveという状態を持っているため、複数のアコーディオンの同期ができていません。
こういう場合にそれぞれのコンポーネントから状態を引き上げるには以下の3つのステップを行います。

  1. 子コンポーネントから状態を削除する。
  2. 共通の親からハードコードされたデータを渡す。
  3. 共通の親に状態を追加してハンドラを併せて子に渡す。

Step 1: Remove state from the child components

子コンポーネントから状態を削除します。
それぞれの子コンポーネントで状態を持っても同期するこはできません。
まず最初のステップは同期したい状態を子から削除し、propsとして受けとるように修正することです。

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

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

Step 2: Pass hardcoded data from the common parent

親コンポーネントからハードコードされたデータを渡します。
子コンポーネントから既に状態は削除し、propsに追加しています。そのため親からハードコードされたデータを渡すよう修正します。
渡す親コンポーネントは、同期したいコンポーネントから最も近接した親コンポーネントです。
Docsの例だと↓な感じですね。

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

ハードコードされた値を調整して変化することを確認します。

Step 3: Add state to the common parent

Docsの例の場合、Activeになるパネルは一つのみなので、状態変数を親コンポーネントに一つ定義します。

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

Docsには状態を引き上げると、その状態の性質が変わってしまうケースがあると書いてあります。
親コンポーネントに状態を定義すると、子から直接状態を更新することができません。
この場合、状態を更新するイベントハンドラをpropsとして子コンポーネントに渡してあげることで解決します。

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

以上の3ステップで状態の引き上げを行うことができました。
実装していると頻繁に使うテクニックだと思います。
複雑な状態管理が必要だったり、コンポーネントの階層が深くなる場合は、useReducerやuseContextなどを組み合わせることにより、明瞭な記述が可能です。

各状態で信頼できる単一の情報源

Reactコンポーネントは各コンポーネントで独自の状態をもつことが多いです。
各ユニークな状態ごとに、それを所有するコンポーネントを選ぶことになります。
これを"Single source of truth"と呼びます。
各ピース(コンポーネント)に対してそのコンポーネントが所有する特定の状態があることを指します。
本章では各コンポーネントで状態を重複させることなく、共通の親に引き上げることでそれをなくすことを学べます。

Your app will change as you work on it. It is common that you will move state down or back up while you’re still figuring out where each piece of the state “lives”. This is all part of the process!

アプリケーションを開発する中で状態を上げ下げすることはプロセスの一部のようです。
実際によく考えてみれば、実装中にこれと同様のことをよく行いますね。

まとめ

状態をコンポーネント間で共有する方法を学びました。
実際開発を行っていると良く行う作業なんですが、手順化するというのは非常に大事ですね。
特に複雑になってくると状態が直復している箇所など出てくると思いますが、こういった手順化しておくことですぐに解消できる部分はあると思います。
基本を大事にして開発をしていきたいものです。

GitHubで編集を提案

Discussion