React + TypeScript: コンテクストでデータを深い階層に渡す

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

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

通常、親コンポーネントから子には、プロパティで情報を渡します。けれど、プロパティを用いることが、冗長で不便になるのがつぎのような場合です。

* 間に多くのコンポーネントを経て渡すことになってしまう(いわゆる「バケツリレー」)。
* アプリケーション内の多くのコンポーネントが同じ情報を必要としている。

コンテクストを使えば、親コンポーネントの情報が、ツリーの下層のどのコンポーネントからでも参照できるようになります。ツリーの深さは問わず、プロパティを介する必要もありません。


# プロパティを渡すときの問題
プロパティは、明示的にUIツリー経由で、データを使用するコンポーネントに渡す優れたやり方です(「[コンポーネントにプロパティを渡す(Passing Props to a Component)](https://qiita.com/FumioNonaka/items/2ba530a906bf97480f80#%E3%82%B3%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88%E3%81%AB%E3%83%97%E3%83%AD%E3%83%91%E3%83%86%E3%82%A3%E3%82%92%E6%B8%A1%E3%81%99passing-props-to-a-component)」参照)。

けれど、プロパティをツリーの奥深くまで渡すときや、多くのコンポーネントが同じプロパティを必要とする場合には、冗長で不便になるかもしれません。もっとも近い共通の親に[状態を引き上げる](https://qiita.com/FumioNonaka/items/611bd5e0cd254c0e9ea0)場合も、データが必要なコンポーネントからはるか上(「バケツリレー」になる)ということがあり得ます。

データをプロパティで渡すことなく、ツリー内の必要としているコンポーネントに「テレポート」できたら、悩みは解消されるでしょう。それを可能にするのが、Reactのコンテクストの機能です。


# コンテクスト: プロパティの受け渡しに替わる機能
コンテクストにより、親コンポーネントからその下のツリー全体にデータが渡せるようになります。コンテクストの途い道はさまざまです。たとえば、`Heading`コンポーネントがあるとしましょう。受け取るプロパティはサイズを決める`level`です、

```react:src/App.tsx
export default function Page() {
	return (
		<Section>
			<Heading level={1}>Title</Heading>
			<Heading level={2}>Heading</Heading>
			<Heading level={3}>Sub-heading</Heading>
			<Heading level={4}>Sub-sub-heading</Heading>
			<Heading level={5}>Sub-sub-sub-heading</Heading>
			<Heading level={6}>Sub-sub-sub-sub-heading</Heading>
		</Section>
	);
}
```

```react:src/Section.tsx
export const Section: FC<Props> = ({ children }) => {
	return <section className="section">{children}</section>;
};
```

```react:src/Heading.tsx
export const Heading: FC<Props> = ({ level, children }) => {
	switch (level) {
		case 1:
			return <h1>{children}</h1>;
		case 2:
			return <h2>{children}</h2>;
		// ...[中略]...
		default:
			throw Error("Unknown level: " + level);
	}
};
```

ここで、コンポーネントの`Section`は入れ子にして、その中に複数の`Heading`を含めることになったとしましょう。`Section`の子の`Heading`は、`level`が同じです。

```react:App.tsx
export default function Page() {
	return (
		<Section>
			<Heading level={1}>Title</Heading>
			<Section>
				<Heading level={2}>Heading</Heading>
				<Heading level={2}>Heading</Heading>
				<Heading level={2}>Heading</Heading>
				<Section>
					<Heading level={3}>Sub-heading</Heading>
					<Heading level={3}>Sub-heading</Heading>
					<Heading level={3}>Sub-heading</Heading>
					<Section>
						<Heading level={4}>Sub-sub-heading</Heading>
						<Heading level={4}>Sub-sub-heading</Heading>
						<Heading level={4}>Sub-sub-heading</Heading>
					</Section>
				</Section>
			</Section>
		</Section>
	);
}
```

だとすると、`Heading`でなく`Section`に`level`プロパティを定めて、まとめてしまえれば端的でしょう。

```react:App.tsx
<Section> level={1}
	<Heading>Title</Heading>
	<Section level={2}>
		<Heading>Heading</Heading>
		<Heading>Heading</Heading>
		<Heading>Heading</Heading>
		<Section level={3}>
			<Heading>Sub-heading</Heading>
			<Heading>Sub-heading</Heading>
			<Heading>Sub-heading</Heading>
			<Section level={4}>
				<Heading>Sub-sub-heading</Heading>
				<Heading>Sub-sub-heading</Heading>
				<Heading>Sub-sub-heading</Heading>
			</Section>
		</Section>
	</Section>
</Section>
```

けれど、コンポーネント`Heading`が親の`Section`のプロパティ`level`を知ることはできません。**子コンポーネントは、プロパティ以外のやり方で、ツリーの上層のデータを「要求」しなければならない**のです。

そこで、コンテクストを使います。手順はつぎの3つです。

1. コンテクストを**作成**します。
    * `level`を定める新たなコンテクストモジュールは`LevelContext`です。
2. データを必要とする子コンポーネントがコンテクストを**使用**しなければなりません。
    * `Heading`が`LevelContext`を使います。
3. コンテクストを**提供**するのは、データを指定する親コンポーネントです。
    * `Section`が`LevelContext`をツリーの下層に与えます。

親の階層がどれだけ離れていても、下層のツリー全体にデータを渡せるのがコンテクストです。

## 手順1: コンテクストを作成する
まず、[`createContext`](https://react.dev/reference/react/createContext#createcontext)でコンテクスト(`LevelContext`)をつくらなければなりません(引数はデフォルト値)。また、コンポーネントが使えるように`export`も必要です。

```react:src/LevelContext.ts
import { createContext } from "react";

export const LevelContext = createContext(1);
```

## 手順2: コンテクストを使用する
モジュール`src/Heading.tsx`は、`useContext`フックと`src/LevelContext.ts`を`import`します。`level`の値はコンテクスト`LevelContext`から得ますので、コンポーネントの引数に`level`プロパティは要りません。

```react:src/Heading.tsx
import { useContext } from 'react';
import { LevelContext } from './LevelContext';

// type Props = { level: number; children: ReactNode };
type Props = { children: ReactNode };
// export const Heading: FC<Props> = ({ level, children }) => {
export const Heading: FC<Props> = ({ children }) => {
	const level = useContext(LevelContext);

};
```

**`useContext`は、読み込みたいコンテクストをReactに示すフックです**。フックを呼び出すのは、コンポーネントかカスタムフック内ののトップレベルでなければなりません(「[状態変数を使う](https://qiita.com/FumioNonaka/items/0961f93d9dcefca7f4dc#%E7%8A%B6%E6%85%8B%E5%A4%89%E6%95%B0%E3%82%92%E4%BD%BF%E3%81%86)」参照)。

`Heading`はプロパティ`level`を持たなくなりました。代わりにプロパティを受け取るのが`Section`コンポーネントです。`Page`コンポーネントが返すJSXはつぎのように書き替えましょう。

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

		</Section> */
		<Section level={1}>
			<Heading>Title</Heading>
			<Section level={2}>
				<Heading>Heading</Heading>
				<Heading>Heading</Heading>
				<Heading>Heading</Heading>
				<Section level={3}>
					<Heading>Sub-heading</Heading>
					<Heading>Sub-heading</Heading>
					<Heading>Sub-heading</Heading>
					<Section level={4}>
						<Heading>Sub-sub-heading</Heading>
						<Heading>Sub-sub-heading</Heading>
						<Heading>Sub-sub-heading</Heading>
					</Section>
				</Section>
			</Section>
		</Section>
	);
}
```

まだ、コンテクスト`LevelContext`の値(`level`)は、`Heading`コンポーネントには渡りません。そのため、`Heading`のテキストはすべて同じサイズになっているでしょう。**コンテクストが提供**されなければ、Reactはどこから値を得ればよいのかわからないからです。

もっとも、エラーにはなっていません。このとき用いられるのが`createContext`の引数に渡したデフォルト値(`1`)です。そのため、すべてのテキストは`<h1>`要素になりました。`Section`コンポーネントごとの`level`の値をコンテクストで提供することにより解決しましょう。

## 手順3: コンテクストを提供する
`Section`コンポーネントがレンダーする`children`に`Heading`は含まれます。それらの子コンポーネントにコンテクストを提供するのがコンテクストプロバイダ[`Provider`](https://react.dev/reference/react/createContext#provider)です。**`LevelContext.Provider`で`children`を包んでください**。`value`が、デフォルト値に替えてコンテクストに定める値です。

```react:src/
import { LevelContext } from './LevelContext';

// type Props = { children: ReactNode };
type Props = { level: number; children: ReactNode };
// export const Section: FC<Props> = ({ children }) => {
export const Section: FC<Props> = ({ level, children }) => {
	return (
		<section className="section">
			<LevelContext.Provider value={level}>{children}</LevelContext.Provider>
		</section>
	);
};
```

これで、Reactは`Section`の中で`LevelContext`が必要な子コンポーネントに`value`の値(`level`)を渡します(サンプル001)。コンテクストプロバイダ(`LevelContext.Provider`)が入れ子になる場合に用いられるのは、UIツリー内をさかのぼってもっとも近いプロバイダの値です。

#### サンプル001■React + TypeScript: Passing Data Deeply with Context 01

https://codesandbox.io/embed/react-typescript-passing-data-deeply-with-context-01-9f13o2?fontsize=14&hidenavigation=1&theme=dark

`level`プロパティは、`Heading`コンポーネントそれぞれから、親の`Section`に移しました。そして、子コンポーネントは直前の親が提供するコンテクストプロバイダから値を得たのです。

1. `level`プロパティは`Section`コンポーネントに与えました。
2. `Section`コンポーネントの中で`children`を包んだのが`LevelContext.Provider`です。
3. `Heading`コンポーネントは、`useContext`で直前の親がプロバイダに定める`LevelContext`の値を取得しました。


# 同じコンポーネントからコンテクストを使用して提供する
それぞれの`Heading`コンポーネントから親の`Section`に`level`プロパティは移せたものの、値はひとつひとつ手入力です。けれど、今回のコード例で`Section`に与えた`level`は、入れ子のひとつ上の親の値に1加えるという規則性があります。

親の`Section`コンポーネントの値は、`useContext`で取得してしまえば、`level`プロパティを受け取る必要はありません。

```react:src/
import { useContext } from 'react';

// type Props = { level: number; children: ReactNode };
type Props = { children: ReactNode };
// export const Section: FC<Props> = ({ level, children }) => {
export const Section: FC<Props> = ({ children }) => {
	const level = useContext(LevelContext);
	return (
		<section className="section">
			{/* <LevelContext.Provider value={level}> */}
			<LevelContext.Provider value={level + 1}>
				{children}
			</LevelContext.Provider>
		</section>
	);
};
```

これで、`level`プロパティは`Section`コンポーネントにも渡さずに済むようになりました(サンプル002)。

```react:src/App.tsx
export default function Page() {
	return (
		// <Section level={1}>
		<Section>

			{/* <Section level={2}> */}
			<Section>

				{/* <Section level={3}> */}
				<Section>

					{/* <Section level={4}> */}
					<Section>

					</Section>
				</Section>
			</Section>
		</Section>
	);
}
```

#### サンプル002■React + TypeScript: Passing Data Deeply with Context 02

https://codesandbox.io/embed/react-typescript-passing-data-deeply-with-context-02-j5ybvd?fontsize=14&hidenavigation=1&theme=dark


# コンテクストは間のコンポーネントを超える
コンテクストを提供するコンポーネントと使うコンポーネントの間には、いくらでも他のコンポーネントを加えて構いません。\<div\>のような組み込みの要素でもReactのコンポーネントでも扱いは同じです。

前の作例(サンプル002)に、新たなコンポーネント`Post`を加えて書き替えてみます。コンポーネント`Heading`が直前の`Section`から得た`level`に応じてテキストサイズを変えるという基本的な仕組みは変わりません。

* `src/AllPosts.tsx`
    - `src/RecentPosts.tsx`
        - `src/Post.tsx`

```react:src/App.tsx
export default function ProfilePage() {
	return (
		<Section>
			<Heading>My Profile</Heading>
			<Post title="Hello traveller!" body="Read about my adventures." />
			<AllPosts />
		</Section>
	);
}
```

`Post`コンポーネントを子にもつ`AllPosts`のツリーも、それぞれ`Section`の中に`Heading`が備わります。

```react:src/AllPosts.tsx
export const AllPosts: FC = () => {
	return (
		<Section>
			<Heading>Posts</Heading>
			<RecentPosts />
		</Section>
	);
};
```

```react:src/RecentPosts.tsx
export const RecentPosts: FC = () => {
	return (
		<Section>
			<Heading>Recent Posts</Heading>
			<Post title="Flavors of Lisbon" body="...those pastéis de nata!" />
			<Post title="Buenos Aires in the rhythm of tango" body="I loved it!" />
		</Section>
	);
};
```

```react:src/Post.tsx
export const Post: FC<Props> = ({ title, body }) => {
	return (
		<Section isFancy={true}>
			<Heading>{title}</Heading>
			<p>
				<i>{body}</i>
			</p>
		</Section>
	);
};
```

`Section`コンポーネントは、オプションで加えた`isFancy`プロパティによって破線の枠がかかるようにしました。

```react:src/Section.tsx
export const Section: FC<Props> = ({ children, isFancy }) => {

	return (
		<section className={"section " + (isFancy ? "fancy" : "")}>

		</section>
	);
};
```

この例でもコンポーネント`Heading`と`Section`の仕組みはそのままですから、`Section`を入れ子にするたびに`Heading`のテキストサイズはひとつずつ下がるでしょう(サンプル003)。

#### サンプル003■React + TypeScript: Passing Data Deeply with Context 03

https://codesandbox.io/embed/react-typescript-passing-data-deeply-with-context-03-idk26u?fontsize=14&hidenavigation=1&theme=dark

**コンテクストを用いることにより、どのコンテクストの下でレンダーされるかに応じてコンポーネントに適用されるデータが変わり、表示も変更できる**のです。

コンテクストの働きは、CSSプロパティの[継承](https://developer.mozilla.org/docs/Web/CSS/inheritance)と似ているかもしれません。`<div>`などの要素に、CSSでプロパティ(たとえば`color: blue`)を定めたとしましょう。すると、その要素の中のDOMノードは、どれだけ深い層でもプロパティを継承します。プロパティ値が変わるのは、中間の別のノードが書き替えた(オーバーライドした)ときです。Reactのコンテクストも、オーバーライドするには子を別のコンテクストプロバイダで包まなければなりません。

CSSでオーバーライドが起こるのは、同じプロパティ同士です。異なるプロパティは互いに影響を与えません。**Reactでも、オーバーライドされるのはコンテクストが同じ場合です**。`createContext()`でつくった異なるコンテクストは、別々に扱われます。そして、それぞれの特定のコンテクストを使用し、提供するコンポーネントに結びつけられるのです。ひとつのコンポーネントがいくつものコンテクストを使用あるいは提供することも問題はありません。


# コンテクストを使う前に
コンテクストは魅力的な機能です。けれど、使い過ぎに注意しましょう。プロパティを深い階層に渡すからといって、コンテクストが情報の適した置き場所とはかぎりません。

コンテクストを使う前に検討すべき案として、ふたつ考えられます。これらでも解決できないときは、コンテクストを検討しましょう。

1. はじめに考えるべきは、[**プロパティを渡す**](https://qiita.com/FumioNonaka/items/2ba530a906bf97480f80#%E3%82%B3%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88%E3%81%AB%E3%83%97%E3%83%AD%E3%83%91%E3%83%86%E3%82%A3%E3%82%92%E6%B8%A1%E3%81%99passing-props-to-a-component)ことです。深い階層にいくつものプロパティを渡すこと自体は、とくにめずらしくはありません。面倒には感じられるでしょう。けれど、どのコンポーネントがどのデータを使うのか明確です。コードを管理する人にとっても、プロパティでデータフローがはっきりして助かるでしょう。
2. **コンポーネントを切り出して`children`としてJSXで渡します**。データを間の(データは使わない)コンポーネントは飛ばして深い階層に渡そうとするとき、コンポーネントが切り出せる場合は少なくありません。たとえば、つぎの`Layout`コンポーネントです。

受け取ったプロパティ(`posts`)は、`Layout`は使いません。データを必要としているのは、子コンポーネントの`Posts`です。

```react:App.tsx
export default function App() {
	return <Layout posts={posts} />;
}
```

```react:Layout.tsx
export const Layout: FC<Props> = ({ posts }) => {
	return <Posts posts={posts} />;
};
```

こういう場合、`Layout`コンポーネントに子ノード`children`を渡してしまえば、プロパティ(`posts`)は`Posts`が直接受け取れます。プロパティのバケツリレーが避けられるのです。

```react:App.tsx
export default function App() {
	return (
		/* <Layout posts={posts} />; */
		<Layout>
			<Posts posts={posts} />
		</Layout>
	);
}
```

```react
// export const Layout: FC<Props> = ({ posts }) => {
export const Layout: FC<Props> = ({ children }) => {
	// return <Posts posts={posts} />;
	return <>{children}</>;
};
```


# コンテクストはどういうときに使えるか

* **テーマ設定**: アプリケーションの外観をテーマ設定(たとえば、ダークモードなど)できるようにする場合に使えます。アプリケーションの最上部に置いたコンテクストプロバイダのデータに応じて、各コンポーネントが表現を合わせればよいでしょう。
* **現行アカウントの管理**: 現在どのユーザーログインしているかは、多くのコンポーネントが必要とすることの多い情報です。コンテクストに加えれば、ツリーのどこからでもデータが得られて便利でしょう。アプリケーションによっては、複数のアカウントを同時に操作するかもしれません(別のユーザーとしてコメントを残したい場合など)。そういうとき、UIの一部を異なる現行アカウントのプロバイダで入れ子にして包むことが考えられます。
* **ルーティング**: ほとんどのルーティングのソリューションが現在のルートを保持するために内部に使っているのはコンテクストです。そうして、それぞれのリンクがアクティブかどうか「把握」します。独自のルーターを構築する場合も、同様にするとよいでしょう。
* **状態の管理**: アプリケーションが大きくなるにつれて、多くの状態は上層部に集まりがちです。他方で、深い下層のコンポーネントが状態を変更しなければならないかもしれません。複雑な状態を管理するには、[コンテクストに加えてリデューサも用いる](https://react.dev/learn/scaling-up-with-reducer-and-context)のが一般的です。さほど手間をかけずに、深い階層のコンポーネントに状態が渡せます。

コンテクストが使われるのは、静的な値にかぎられません。つぎのレンダリング時の値が変われば、Reactはその値にもとづいてすべての下層のコンポーネントを更新します。コンテクストが状態とともに用いられるのはそのためです。

一般に、ツリーの中の離れたばらばらの場所から情報が必要とされるとき、コンテクストは役立つでしょう。


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

* コンテクストにより、コンポーネントはその下層のツリー全体に情報が与えられます。
* コンテクストを渡す手順は3つです。
    1. つぎのコードでコンテクストを作成します。
`export const MyContext = createContext(defaultValue)`
    2. 階層の深さに関わりなく、子コンポーネントからコンテクストを使用するために呼び出すのがフック`useContext(MyContext)`です。
    3. 親から子にコンテクストを提供するには、ツリーはプロバイダ`<MyContext.Provider value={...}>`で包んでください。
* コンテクストはツリーの中間のコンポーネントを経由しません。
* コンテクストを用いることにより、レンダーされる「状況に応じた」コンポーネントがつくれます。
* コンテクストはいきなり使うのでなく、情報はプロパティとして渡したり、`children`でJSXを与えることも考えてみてください。