2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-05-23

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

なお、本シリーズ解説の他の記事については「React + TypeScript: React公式ドキュメントの基本解説『Learn React』を学ぶ」をご参照ください。

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

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

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

プロパティを渡すときの問題

プロパティは、明示的にUIツリー経由で、データを使用するコンポーネントに渡す優れたやり方です(「コンポーネントにプロパティを渡す(Passing Props to a Component)」参照)。

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

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

コンテクスト: プロパティの受け渡しに替わる機能

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

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>
	);
}
src/Section.tsx
export const Section: FC<Props> = ({ children }) => {
	return <section className="section">{children}</section>;
};
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が同じです。

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でなくSectionlevelプロパティを定めて、まとめてしまえれば端的でしょう。

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. データを必要とする子コンポーネントがコンテクストを使用しなければなりません。
    • HeadingLevelContextを使います。
  3. コンテクストを提供するのは、データを指定する親コンポーネントです。
    • SectionLevelContextをツリーの下層に与えます。

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

手順1: コンテクストを作成する

まず、createContextでコンテクスト(LevelContext)をつくらなければなりません(引数はデフォルト値)。また、コンポーネントが使えるようにexportも必要です。

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

export const LevelContext = createContext(1);

手順2: コンテクストを使用する

モジュールsrc/Heading.tsxは、useContextフックとsrc/LevelContext.tsimportします。levelの値はコンテクストLevelContextから得ますので、コンポーネントの引数にlevelプロパティは要りません。

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に示すフックです。フックを呼び出すのは、コンポーネントかカスタムフック内ののトップレベルでなければなりません(「状態変数を使う」参照)。

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

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コンポーネントがレンダーするchildrenHeadingは含まれます。それらの子コンポーネントにコンテクストを提供するのがコンテクストプロバイダProviderです。LevelContext.Providerchildrenを包んでくださいvalueが、デフォルト値に替えてコンテクストに定める値です。

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

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

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

同じコンポーネントからコンテクストを使用して提供する

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

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

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)。

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

コンテクストは間のコンポーネントを超える

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

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

  • src/AllPosts.tsx
    • src/RecentPosts.tsx
      • src/Post.tsx
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が備わります。

src/AllPosts.tsx
export const AllPosts: FC = () => {
	return (
		<Section>
			<Heading>Posts</Heading>
			<RecentPosts />
		</Section>
	);
};
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>
	);
};
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プロパティによって破線の枠がかかるようにしました。

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

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

		</section>
	);
};

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

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

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

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

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

コンテクストを使う前に

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

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

  1. はじめに考えるべきは、プロパティを渡すことです。深い階層にいくつものプロパティを渡すこと自体は、とくにめずらしくはありません。面倒には感じられるでしょう。けれど、どのコンポーネントがどのデータを使うのか明確です。コードを管理する人にとっても、プロパティでデータフローがはっきりして助かるでしょう。
  2. コンポーネントを切り出してchildrenとしてJSXで渡します。データを間の(データは使わない)コンポーネントは飛ばして深い階層に渡そうとするとき、コンポーネントが切り出せる場合は少なくありません。たとえば、つぎのLayoutコンポーネントです。

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

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

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

App.tsx
export default function App() {
	return (
		/* <Layout posts={posts} />; */
		<Layout>
			<Posts posts={posts} />
		</Layout>
	);
}
// export const Layout: FC<Props> = ({ posts }) => {
export const Layout: FC<Props> = ({ children }) => {
	// return <Posts posts={posts} />;
	return <>{children}</>;
};

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

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

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

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

まとめ

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

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?