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
です、
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>
);
}
export const Section: FC<Props> = ({ children }) => {
return <section className="section">{children}</section>;
};
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
が同じです。
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
プロパティを定めて、まとめてしまえれば端的でしょう。
<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つです。
- コンテクストを作成します。
-
level
を定める新たなコンテクストモジュールはLevelContext
です。
-
- データを必要とする子コンポーネントがコンテクストを使用しなければなりません。
-
Heading
がLevelContext
を使います。
-
- コンテクストを提供するのは、データを指定する親コンポーネントです。
-
Section
がLevelContext
をツリーの下層に与えます。
-
親の階層がどれだけ離れていても、下層のツリー全体にデータを渡せるのがコンテクストです。
手順1: コンテクストを作成する
まず、createContext
でコンテクスト(LevelContext
)をつくらなければなりません(引数はデフォルト値)。また、コンポーネントが使えるようにexport
も必要です。
import { createContext } from "react";
export const LevelContext = createContext(1);
手順2: コンテクストを使用する
モジュールsrc/Heading.tsx
は、useContext
フックとsrc/LevelContext.ts
をimport
します。level
の値はコンテクストLevelContext
から得ますので、コンポーネントの引数にlevel
プロパティは要りません。
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はつぎのように書き替えましょう。
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
です。LevelContext.Provider
でchildren
を包んでください。value
が、デフォルト値に替えてコンテクストに定める値です。
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
に移しました。そして、子コンポーネントは直前の親が提供するコンテクストプロバイダから値を得たのです。
-
level
プロパティはSection
コンポーネントに与えました。 -
Section
コンポーネントの中でchildren
を包んだのがLevelContext.Provider
です。 -
Heading
コンポーネントは、useContext
で直前の親がプロバイダに定めるLevelContext
の値を取得しました。
同じコンポーネントからコンテクストを使用して提供する
それぞれのHeading
コンポーネントから親のSection
にlevel
プロパティは移せたものの、値はひとつひとつ手入力です。けれど、今回のコード例でSection
に与えたlevel
は、入れ子のひとつ上の親の値に1加えるという規則性があります。
親のSection
コンポーネントの値は、useContext
で取得してしまえば、level
プロパティを受け取る必要はありません。
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)。
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
-
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
が備わります。
export const AllPosts: FC = () => {
return (
<Section>
<Heading>Posts</Heading>
<RecentPosts />
</Section>
);
};
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>
);
};
export const Post: FC<Props> = ({ title, body }) => {
return (
<Section isFancy={true}>
<Heading>{title}</Heading>
<p>
<i>{body}</i>
</p>
</Section>
);
};
Section
コンポーネントは、オプションで加えたisFancy
プロパティによって破線の枠がかかるようにしました。
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
コンテクストを用いることにより、どのコンテクストの下でレンダーされるかに応じてコンポーネントに適用されるデータが変わり、表示も変更できるのです。
コンテクストの働きは、CSSプロパティの継承と似ているかもしれません。<div>
などの要素に、CSSでプロパティ(たとえばcolor: blue
)を定めたとしましょう。すると、その要素の中のDOMノードは、どれだけ深い層でもプロパティを継承します。プロパティ値が変わるのは、中間の別のノードが書き替えた(オーバーライドした)ときです。Reactのコンテクストも、オーバーライドするには子を別のコンテクストプロバイダで包まなければなりません。
CSSでオーバーライドが起こるのは、同じプロパティ同士です。異なるプロパティは互いに影響を与えません。Reactでも、オーバーライドされるのはコンテクストが同じ場合です。createContext()
でつくった異なるコンテクストは、別々に扱われます。そして、それぞれの特定のコンテクストを使用し、提供するコンポーネントに結びつけられるのです。ひとつのコンポーネントがいくつものコンテクストを使用あるいは提供することも問題はありません。
コンテクストを使う前に
コンテクストは魅力的な機能です。けれど、使い過ぎに注意しましょう。プロパティを深い階層に渡すからといって、コンテクストが情報の適した置き場所とはかぎりません。
コンテクストを使う前に検討すべき案として、ふたつ考えられます。これらでも解決できないときは、コンテクストを検討しましょう。
- はじめに考えるべきは、プロパティを渡すことです。深い階層にいくつものプロパティを渡すこと自体は、とくにめずらしくはありません。面倒には感じられるでしょう。けれど、どのコンポーネントがどのデータを使うのか明確です。コードを管理する人にとっても、プロパティでデータフローがはっきりして助かるでしょう。
-
コンポーネントを切り出して
children
としてJSXで渡します。データを間の(データは使わない)コンポーネントは飛ばして深い階層に渡そうとするとき、コンポーネントが切り出せる場合は少なくありません。たとえば、つぎのLayout
コンポーネントです。
受け取ったプロパティ(posts
)は、Layout
は使いません。データを必要としているのは、子コンポーネントのPosts
です。
export default function App() {
return <Layout posts={posts} />;
}
export const Layout: FC<Props> = ({ posts }) => {
return <Posts posts={posts} />;
};
こういう場合、Layout
コンポーネントに子ノードchildren
を渡してしまえば、プロパティ(posts
)はPosts
が直接受け取れます。プロパティのバケツリレーが避けられるのです。
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つです。
- つぎのコードでコンテクストを作成します。
export const MyContext = createContext(defaultValue)
- 階層の深さに関わりなく、子コンポーネントからコンテクストを使用するために呼び出すのがフック
useContext(MyContext)
です。 - 親から子にコンテクストを提供するには、ツリーはプロバイダ
<MyContext.Provider value={...}>
で包んでください。
- つぎのコードでコンテクストを作成します。
- コンテクストはツリーの中間のコンポーネントを経由しません。
- コンテクストを用いることにより、レンダーされる「状況に応じた」コンポーネントがつくれます。
- コンテクストはいきなり使うのでなく、情報はプロパティとして渡したり、
children
でJSXを与えることも考えてみてください。