React + TypeScript: コンポーネント間で状態を共有する

React公式サイトのドキュメントが2023年3月16日に改訂されました(「[Introducing react.dev](https://react.dev/blog/2023/03/16/introducing-react-dev)」参照)。本稿は、基本解説の「[Sharing State Between Components](https://react.dev/learn/sharing-state-between-components)」をかいつまんでまとめた記事です。ただし、コードにはTypeScriptを加えました。反面、初心者向けのJavaScriptの基礎的な説明は省いています。

なお、本シリーズ解説の他の記事については「[React + TypeScript: React公式ドキュメントの基本解説『Learn React』を学ぶ](https://qiita.com/FumioNonaka/items/d70585bd3c0a6d4edc24)」をご参照ください。

ふたつのコンポーネントの状態を、つねに一緒に変化させたい場合があるでしょう。そのためには、状態をそれぞれのコンポーネントにもたせるのでなく、もっとも近い共通の親に移すべきです。そのうえで、親から子には値をプロパティで渡します。これは状態の引き上げ(リフトアップ)と呼ばれ、Reactのコードでよく用いられるやり方です。


# 状態を引き上げる例
ふたつの別々のパネル(`Panel`)が子コンポーネントとして開く、アコーディオン(`Accordion`)のコンポーネントをつくってみましょう。

* `Accordion`
    - `Panel`
    - `Panel`

`Panel`コンポーネントにはそれぞれブール値の状態変数`isActive`が備わり、コンテンツをパネルに表示するかどうか決まります。[Show]ボタンを押すとパネルが開くでしょう(サンプル001)。

```react:src/Panel.tsx
type Props = { title: string; children: ReactNode };
export const Panel: FC<Props> = ({ 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>
	);
};
```

```react:src/App.tsx
export default function Accordion() {
	return (
		<>

			<Panel title="About">
				...[略]...
			</Panel>
			<Panel title="Etymology">
				...[略]...
			</Panel>
		</>
	);
}
```

#### サンプル001■React + TypeScript: Sharing State Between Components 01

https://codesandbox.io/embed/react-typescript-sharing-state-between-components-01-i8ubde?fontsize=14&hidenavigation=1&theme=dark

ボタンはもう一方のパネルには影響しません。状態変数`isActive`はそれぞれのコンポーネントがもち、互いに独立しているからです。

* `Accordion`
    - `Panel`
        - `isActive`
    - `Panel`
        - `isActive`

さてここで、**つねにひとつのパネルだけが開いている**ように変更しましょう。すると、ひとつを開いたら、もうひとつはたたまなければなりません。そのとき用いるのが、親コンポーネントへの状態の引き上げです。つぎの3つの手順で進めます。

* 状態は子コンポーネントから**除いて**ください。
* データは共通の親コンポーネントから**渡します**。
* 共通の親コンポーネントに状態を**加え**、イベントハンドラとともに子に与えましょう。

こうして、`Accordion`コンポーネントはふたつのパネルを連携し、つねにひとつだけ開けるようになるのです。

## 手順1: 状態を子コンポーネントから除く
`Panel`の`isActive`の制御は、親コンポーネント(`Accordion`)に委ねましょう。つまり、`isActive`は親コンポーネントが、子の`Panel`にプロパティとして与えるのです。`Panel`コンポーネントの状態は削り、親から引数で受け取るプロパティに`isActive`を加えます。

```react:src/Panel.tsx
// export const Panel: FC<Props> = ({ title, children }) => {
export const Panel: FC<Props> = ({ title, children, isActive }) => {
	// const [isActive, setIsActive] = useState(false);

};
```

`Panel`コンポーネントは`isActive`を変更できなくなり、親が共通に渡すプロパティ値にしたがうようになりました。

## 手順2: データを共通の親コンポーネントから渡す
状態の引き上げのためには、連携したいコンポーネントにもっとも近い共通の親を探し出さなければなりません。今回の例では単純に`Accordion`コンポーネントです。連携させる`Panel`に共通のプロパティ値が与えられるので、「[信頼できる情報源](https://ja.wikipedia.org/wiki/%E4%BF%A1%E9%A0%BC%E3%81%A7%E3%81%8D%E3%82%8B%E5%94%AF%E4%B8%80%E3%81%AE%E6%83%85%E5%A0%B1%E6%BA%90)」(source of truth)になります。

* `Accordion` (もっとも近い共通の親)
    - `<Panel isActive={value}>`
        - `const Panel = ({ isActive }) => {}`
        - `const Panel = ({ isActive }) => {}`

`Accordion`コンポーネントから子の`Panel`にプロパティ`isActive`を渡すように書き替えたのがつぎのコードです。もっとも、値を固定の`true`にしましたので、パネルは閉じません。また、今は`Panel`の`onClick`イベントハンドラも使えなくなりました。

```react:src/App.tsx
export default function Accordion() {
	return (
		<>

			<Panel title="About" isActive={true}>
				...[略]...
			</Panel>
			<Panel title="Etymology" isActive={true}>
				...[略]...
			</Panel>
		</>
	);
}
```

```react:src/Panel.tsx
type Props = { title: string; children: ReactNode; isActive: boolean };
export const Panel: FC<Props> = ({ title, children, isActive }) => {
	return (
		<section className="panel">
			<h3>{title}</h3>
			{isActive ? (
				<p>{children}</p>
			) : (
				<button>Show</button>
			)}
		</section>
	);
};
```

## 手順3: 共通の親コンポーネントに状態を加える
状態を引き上げると、もともともっていた状態と性質が変わることも少なくありません。一度に開くのはひとつのパネルとするには、共通の親である`Accordion`コンポーネントはどのパネルがアクティブなのかを知っているべきでしょう。そこで、親コンポーネントの状態は、ブール値でなく、開いているパネルを示す数値インデックスとします。

`activeIndex`の初期値は0なので、はじめに開くのは上のパネルです。パネルの[Show]ボタンをクリックするたびに、状態変数値は切り替えなければなりません。けれど、子コンポーネント`Panel`は、親がもつ状態変数を直に変えることは禁じられます。そこで、`Accordion`コンポーネントから子の`Panel`に`onShow`「[イベントハンドラをプロパティとして渡す](https://qiita.com/FumioNonaka/items/3414af535faf2c924786#%E3%82%A4%E3%83%99%E3%83%B3%E3%83%88%E3%83%8F%E3%83%B3%E3%83%89%E3%83%A9%E3%82%92%E3%83%97%E3%83%AD%E3%83%91%E3%83%86%E3%82%A3%E3%81%A8%E3%81%97%E3%81%A6%E6%B8%A1%E3%81%99)」ことにより変更が許されるようになるのです(サンプル002)。

```react:src/App.tsx
export default function Accordion() {
	const [activeIndex, setActiveIndex] = useState(0);
	return (
		<>

			{/* <Panel title="About" isActive={true}> */}
			<Panel
				title="About"
				isActive={activeIndex === 0}
				onShow={() => setActiveIndex(0)}
			>
				...[略]...
			</Panel>
			{/* <Panel title="Etymology" isActive={true}> */}
			<Panel
				title="Etymology"
				isActive={activeIndex === 1}
				onShow={() => setActiveIndex(1)}
			>
				...[略]...
			</Panel>
		</>
	);
}
```

```react:src/Panel.tsx
// type Props = { title: string; children: ReactNode; isActive: boolean };
type Props = {
	title: string;
	children: ReactNode;
	isActive: boolean;
	onShow: () => void;
};
// export const Panel: FC<Props> = ({ title, children, isActive }) => {
export const Panel: FC<Props> = ({ title, children, isActive, onShow }) => {
	return (
		<section className="panel">

			{isActive ? (
				<p>{children}</p>
			) : (
				// <button>
				<button onClick={onShow}>Show</button>
			)}
		</section>
	);
};
```

#### サンプル002React + TypeScript: Sharing State Between Components 02

https://codesandbox.io/embed/react-typescript-sharing-state-between-components-02-ssu4ni?fontsize=14&hidenavigation=1&theme=dark

状態を子コンポーネントにもっとも近い共通の親に引き上げました。すると、共通の状態にもとづいて、子コンポーネントが連携させられるのです。

* 親コンポーネント`Accordion`は状態`activeIndex`をもちます。
    - 子コンポーネント`Panel`に渡される`isActive`は、その状態から算出されたプロパティです。
    - 子コンポーネント`Panel`にはさらに、状態変数`activeIndex`を設定するイベントハンドラ`onShow`も渡しました。
* 子コンポーネント`Panel`は親からプロパティとして、`isActive`と`onShow`を受け取ります。
    - 開くべきパネルを統一的に定めるのが`isActive`です。
    - ボタンで切り替えるパネルの開閉は、与えられたイベントハンドラ`onShow`により、状態変更そのものは親に委ねます。

### コンポーネントがコントロールされているかいないか
状態をローカルにもつコンポーネントは「コントロールされていない」と呼ぶのが一般的です。たとえば、前掲サンプル001の`Panel`コンポーネントがそうでしょう。状態変数`isActive`をみずからの中にもつので、親コンポーネントも状態が制御できないからです。

これに対して、コンポーネントの重要な情報が、自分のローカルな状態でなく、プロパティにより操作されている場合「コントロールされている」といえるでしょう。親コンポーネントが与えるプロパティで、ふるまいが決められるからです。前掲サンプル002の`Panel`コンポーネントは、親の`Accordion`から渡される`isActive`プロパティで制御されていました。

制御されていないコンポーネントは、設定があまり要りませんので、親の中で使うには簡単です。けれど、コンポーネント間で連携をとるといった柔軟性が損なわれます。制御されたコンポーネントなら柔軟性は高いものの、親コンポーネントがプロパティを細かく設定しなければならないのです。

実際のところ、「コントロールされている」とか「コントロールされていない」というのは、厳密な技術用語とはいえません。多くのコンポーネントは、ローカルな状態とプロパティをともに備えます。ただ、コンポーネントの設計や提供する機能を考えるときには役立つでしょう。コンポーネントは、どの情報を(プロパティで)コントロールするか、どの情報は(ローカルな状態にして)コントロールしないか考えてつくることが大切です。


# 状態の信頼できる唯一の情報源
Reactアプリケーションの中で、コンポーネントはそれぞれの状態をもちます。ある状態はツリーの下層のコンポーネントに属するかもしれません。アプリケーションの最上層に備わる状態もあるでしょう。たとえば、クライアントサイドのルーティングライブラリは、現行ルートをReactの状態に実装します。そのうえで、プロパティとして下層に渡すのです。

**個別の状態ごとに、どのコンポーネントにそれを「保持」させるか選ばなければなりません**。「[信頼できる唯一の情報源](https://ja.wikipedia.org/wiki/%E4%BF%A1%E9%A0%BC%E3%81%A7%E3%81%8D%E3%82%8B%E5%94%AF%E4%B8%80%E3%81%AE%E6%83%85%E5%A0%B1%E6%BA%90)」と呼ばれる原則です。すべての状態をひとつの場所にまとめなければならないということではありません。けれど、特定の状態ごとに、その情報を備えるコンポーネントがあるということです。コンポーネント間で共有する状態は重複させるのでなく、それらの共通する親に引き上げ、必要とする子にはプロパティで渡しましょう。作業が進んでアプリケーションが変化したら、状態のもたせ方は再検討すればよいのです。


# まとめ
この記事では、つぎのような項目についてご説明しました。

* ふたつのコンポーネントを連携したいときは、それらが用いる状態は共通の親に引き上げましょう。
* 共通の親コンポーネントに引き上げた状態から算出した連携に必要な情報は、子にプロパティとして渡します。
* 親コンポーネントの状態を子から変更するには、与えるプロパティにイベントハンドラも含めてください。
* コンポーネントを(プロパティで)「コントロールされている」か(ローカルな状態で)「コントロールされていないか」考えることは有用です。