コンテクストで深くデータを受け渡す
通常、親コンポーネントから子コンポーネントには props を使って情報を渡します。しかし、props を多数の中間コンポーネントを経由して渡さないといけない場合や、アプリ内の多くのコンポーネントが同じ情報を必要とする場合、props の受け渡しは冗長で不便なものとなり得ます。コンテクスト (Context) を使用することで、親コンポーネントから props を明示的に渡さずとも、それ以下のツリー内の任意のコンポーネントが情報を受け取れるようにできます。
このページで学ぶこと
- “props の穴掘り作業 (prop drilling)” とは何か
- コンテクストを使って props の冗長な受け渡しを解消する方法
- コンテクストの一般的な用途
- コンテクストの代替手段
props の受け渡しに伴う問題
props の受け渡しは、UI ツリー内でデータを取り回してそれを必要とするコンポーネントに届けるための素晴らしい手法です。
しかし、props をツリー内の深い位置にまで渡す必要がある場合や、多くのコンポーネントが同じ props を必要としている場合、props の受け渡しは冗長で不便なものとなり得ます。最も近い共通の祖先要素は、データを必要としているコンポーネントから遠く離れているかもしれず、そのような高い位置まで state をリフトアップしていくと “props の穴掘り作業 (prop drilling)” と呼ばれる状況に陥ることがあります。
もし props を受け渡すことなしに、データを必要としているツリー内のコンポーネントに直接データを「テレポート」させる方法があれば、素晴らしいと思いませんか? React のコンテクスト機能を使えば、それが可能です!
コンテクスト:props 受け渡しの代替手段
コンテクストを使うことで、親コンポーネントが配下のツリー全体にデータを提供できます。コンテクストには多くの用途がありますが、以下に一例を示します。見出しサイズを表す level
を受け取る Heading
というコンポーネントを考えてみましょう。
import Heading from './Heading.js'; import Section from './Section.js'; 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> ); }
ここで、同じ Section
内の複数の見出しは常に同じサイズである必要があるとしましょう。
import Heading from './Heading.js'; import Section from './Section.js'; 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> ); }
今のところ、level
を props としてそれぞれの <Heading>
に個別に渡しています。
<Section>
<Heading level={3}>About</Heading>
<Heading level={3}>Photos</Heading>
<Heading level={3}>Videos</Heading>
</Section>
もし level
を個々の <Heading>
から削除して、代わりに <Section>
コンポーネントに渡すことができれば素敵ですね。これにより同じセクション内にあるすべての見出しが同じサイズになることを強制できそうです。
<Section level={3}>
<Heading>About</Heading>
<Heading>Photos</Heading>
<Heading>Videos</Heading>
</Section>
ですが <Heading>
コンポーネントは、最も近い <Section>
のレベルをどうやって知ることができるのでしょうか? これには、ツリー上部のどこかにあるデータを子コンポーネントが「要求する」方法が必要です。
これは props だけでは実現できません。ここで登場するのがコンテクストです。以下の 3 つの手順で実現できます。
- コンテクストを作成する。(ここでは見出しレベル用なので
LevelContext
と呼ぶ。) - データを必要とするコンポーネントがそのコンテクストを使用 (use) する。(
Heading
がLevelContext
を使用する。) - データを指定するコンポーネントがそのコンテクストを提供 (provide) する。(
Section
がLevelContext
を提供する。)
コンテクストを使うと、離れた親コンポーネントからであっても、その内部のツリー全体にデータを提供できます。
ステップ 1:コンテクストを作成する
まずはコンテクストを作成する必要があります。複数のコンポーネントから使うことができるように、ファイルを作ってコンテクストをエクスポートする必要があります。
import { createContext } from 'react'; export const LevelContext = createContext(1);
createContext
の唯一の引数はデフォルト値です。ここでは、1
という値は最大の見出しに対応するレベルを示していますが、任意の型の値(オブジェクトでも)を渡すことができます。デフォルト値の意味は次のステップで分かります。
ステップ 2:コンテクストを使用する
React の useContext
フックと、作ったコンテクストをインポートします。
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';
現在、Heading
コンポーネントは level
を props から読み取っています。
export default function Heading({ level, children }) {
// ...
}
代わりに、props から level
を削除し、さきほどインポートした LevelContext
から値を読み取るようにします。
export default function Heading({ children }) {
const level = useContext(LevelContext);
// ...
}
useContext
はフックです。useState
や useReducer
も同じですが、フックは React コンポーネント内のトップレベルでのみ呼び出すことができます(ループや条件分岐の中では呼び出せません)。useContext
は、Heading
コンポーネントが LevelContext
を読み取りたいのだということを React に伝えています。
もう Heading
コンポーネントの props から level
がなくなったので、以下のように JSX で Heading
にレベルを渡す必要はありません。
<Section>
<Heading level={4}>Sub-sub-heading</Heading>
<Heading level={4}>Sub-sub-heading</Heading>
<Heading level={4}>Sub-sub-heading</Heading>
</Section>
JSX を更新して、代わりに Section
が level
を受け取るようにしましょう。
<Section level={4}>
<Heading>Sub-sub-heading</Heading>
<Heading>Sub-sub-heading</Heading>
<Heading>Sub-sub-heading</Heading>
</Section>
改めて、動作させたいコードを以下に再掲します。
import Heading from './Heading.js'; import Section from './Section.js'; export default function Page() { return ( <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> ); }
この例はまだちゃんと動作していません。すべての見出しが同じサイズになってしまっているのは、コンテクストの使用はしているが、まだコンテクストの提供を行っていないからです。React はどこから値を取得すればいいのか分かりません!
コンテクストが提供されていない場合、React は前のステップで指定したデフォルト値を使用します。この例では、createContext
の引数に 1
を指定していましたので、useContext(LevelContext)
は 1
を返し、そのためこれらの見出しは全部 <h1>
になってしまっています。これを修正するため、各 Section
がそれぞれコンテクストを提供するようにしましょう。
ステップ 3:コンテクストを提供する
現在 Section
コンポーネントは子要素を直接レンダーしています。
export default function Section({ children }) {
return (
<section className="section">
{children}
</section>
);
}
これをコンテクストプロバイダでラップし、LevelContext
の値を提供します。
import { LevelContext } from './LevelContext.js';
export default function Section({ level, children }) {
return (
<section className="section">
<LevelContext.Provider value={level}>
{children}
</LevelContext.Provider>
</section>
);
}
これにより、「この <Section>
の下にあるコンポーネントが LevelContext
の値を要求した場合、この level
を渡せ」と React に伝えていることになります。コンポーネントは、UI ツリー内の上側で、最も近い <LevelContext.Provider>
の値を使用します。
import Heading from './Heading.js'; import Section from './Section.js'; export default function Page() { return ( <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> ); }
元のコードと見た目の結果は同じですが、level
を props として個々の Heading
コンポーネントに渡さずに済んでいます! 代わりに、見出しは最も近い Section
に値を要求して、自分の見出しレベルを自分で「判断」しているのです。
<Section>
に props としてlevel
を渡す。Section
は子要素を<LevelContext.Provider value={level}>
でラップする。Heading
はuseContext(LevelContext)
とすることで、上にある最も近いLevelContext
の値を要求する。
同一コンポーネントでコンテクストを使用しつつ提供
今はまだ、セクションごとに level
を手動で指定する必要があります。
export default function Page() {
return (
<Section level={1}>
...
<Section level={2}>
...
<Section level={3}>
...
コンテクストによりコンポーネントの上部から情報を読み取ることができるため、各 Section
は上にある別の Section
から level
を読み取りつつ、level + 1
を下に渡すことができます。以下のようになります。
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';
export default function Section({ children }) {
const level = useContext(LevelContext);
return (
<section className="section">
<LevelContext.Provider value={level + 1}>
{children}
</LevelContext.Provider>
</section>
);
}
この変更により、<Section>
や <Heading>
のどちらにも props として level
を渡さずに済むようになりました。
import Heading from './Heading.js'; import Section from './Section.js'; export default function Page() { return ( <Section> <Heading>Title</Heading> <Section> <Heading>Heading</Heading> <Heading>Heading</Heading> <Heading>Heading</Heading> <Section> <Heading>Sub-heading</Heading> <Heading>Sub-heading</Heading> <Heading>Sub-heading</Heading> <Section> <Heading>Sub-sub-heading</Heading> <Heading>Sub-sub-heading</Heading> <Heading>Sub-sub-heading</Heading> </Section> </Section> </Section> </Section> ); }
Heading
と Section
の両方が、LevelContext
を読み取ることで自分がどの「深さ」にいるかを判断しています。また、Section
はその子要素を LevelContext
プロバイダ内にラップすることで、その中のすべてのものが「1 段階深い」レベルになるよう指定します。
コンテクストは中間コンポーネントを貫通する
コンテクストを提供するコンポーネントとそれを利用するコンポーネントの間には、好きなだけコンポーネントを挿入することができます。<div>
などの組み込みコンポーネントでも、自分で構築するコンポーネントでも構いません。
この例では、同じ Post
コンポーネント(破線ボーダー)が、2 つの異なるネストレベルでレンダーされています。Post
の中にある <Heading>
が、最も近くにある <Section>
から自動的に見出しレベルを取得できていることに注目してください:
import Heading from './Heading.js'; import Section from './Section.js'; export default function ProfilePage() { return ( <Section> <Heading>My Profile</Heading> <Post title="Hello traveller!" body="Read about my adventures." /> <AllPosts /> </Section> ); } function AllPosts() { return ( <Section> <Heading>Posts</Heading> <RecentPosts /> </Section> ); } function RecentPosts() { 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> ); } function Post({ title, body }) { return ( <Section isFancy={true}> <Heading> {title} </Heading> <p><i>{body}</i></p> </Section> ); }
これを動作させるために特別なことは何もしていません。Section
が内部のツリーに対するコンテクストを指定しているため、どこにでも <Heading>
を挿入することができ、正しいサイズで表示されます。上のサンドボックスで試してみてください!
コンテクストを使うことで、どんな場所で、すなわちどんな文脈(コンテクスト)でレンダーされているかに応じて異なる内容を表示できる、「周囲に適応」するコンポーネントを書けるようになります。
コンテクストの仕組みは、CSS におけるプロパティの継承に似ていると感じるかもしれません。CSS では、ある <div>
に対して color: blue
を指定すると、他の DOM ノードが途中で color: green
などで上書きしない限り、どんなに深くネストされた DOM ノードにもその色が継承されます。同様に React では、上からやってくるコンテクストを上書きする唯一の方法は、別の値を指定したコンテクストプロバイダで子をラップすることです。
CSS では、color
や background-color
といった異なるプロパティがお互いを上書きすることはありません。すべての <div>
の color
を赤に設定しても、background-color
には影響しません。同様に、異なる React コンテクストはお互いを上書きしません。createContext()
で作成したそれぞれのコンテクストは、他のものと完全に切り離されており、その特定のコンテクストを使用あるいは提供しているコンポーネント同士だけを結びつけます。1 つのコンポーネントが、異なるコンテクストをいくつも使用しても、あるいは提供しても、問題ありません。
コンテクストを使う前に
コンテクストはとても魅力的です! しかし、これはコンテクストは使いすぎにつながりやすいということでもあります。いくつかの props を数レベルの深さにわたって受け渡す必要があるというだけでは、その情報をコンテクストに入れるべきとはいえません。
ここで紹介するように、コンテクストを使う前に検討すべきいくつかの代替案があります。
- まずは props を渡す方法から始めましょう。ちょっと凝ったコンポーネントであれば、多くの props を多くのコンポーネントを通して受け渡すことは珍しくありません。退屈な仕事に感じるかもしれませんが、どのコンポーネントがどのデータを使っているかが非常に明確になります! コードをメンテナンスする人は、props を使ってデータの流れが明確に表現されていることに感謝するでしょう。
- コンポーネントを抽出して、
children
を JSX として渡す方法を検討しましょう。もし、何らかのデータを、それを必要とせずただ下に流すだけの中間コンポーネントを何層も経由して受け渡ししているような場合、何かコンポーネントを抽出するのを忘れているということかもしれません。たとえば、<Layout posts={posts} />
のような形で、データを直接使わないビジュアルコンポーネントにpost
のようなデータを渡しているのかもしれません。代わりに、Layout
はchildren
を props として受け取るようにし、<Layout><Posts posts={posts} /></Layout>
のようにレンダーしてみましょう。これにより、データを指定するコンポーネントとそれを必要とするコンポーネントの間のレイヤ数が減ります。
これらのアプローチがどちらもうまくいかない場合は、コンテクストを検討してください。
コンテクストのさまざまな用途
- テーマ:例えばダークモードのように、アプリの外見をユーザが変更できる場合は、アプリのトップレベルにコンテクストプロバイダを配置し、外観を変化させる必要があるコンポーネントでそのコンテクストを使用します。
- 現在のアカウント:多くのコンポーネントは、現在ログインしているユーザを知る必要があります。それをコンテクストに入れることで、ツリーのどこからでも読み取りが容易になります。一部のアプリでは、複数のアカウントを同時に操作できます(例:別のユーザとしてコメントを残す)。このような場合、別の現在アカウントを指定したプロバイダをネストして、UI の一部をラップすることが有用です。
- ルーティング:ほとんどのルーティングソリューションは、現在のルートを保持するために内部でコンテクストを使用しています。これが、自身がアクティブかどうかをすべてのリンクが「知っている」理由です。独自のルータを構築する場合は自分でもこれを行いたいでしょう。
- state 管理:アプリが大きくなると、アプリのトップ近くに大量の state が集まってくることがあります。下の遠いところにある多くのコンポーネントがその state 変更する必要があるかもしれません。リデューサとコンテクストを一緒に使用することは一般的であり、これにより大変な手間をかけずに、複雑な state を離れたコンポーネントへ受け渡すことができます。
コンテクストで扱う値は静的なものとは限りません。次のレンダーで異なる値を渡すと、React はその下でそれを必要しているすべてのコンポーネントを更新します! これがコンテクストが state と一緒によく使われる理由です。
一般的に、ある情報が、ツリーの様々な部分にある離れたコンポーネントによって必要とされている場合、コンテクストが役立つというサインです。
まとめ
- コンテクストにより、コンポーネントがそれ以下のツリー全体に情報を提供できる。
- コンテクストを使うには:
export const MyContext = createContext(defaultValue)
を使用して作成およびエクスポートする。- フックに
useContext(MyContext)
のようにコンテクストを渡せば、どんな深い子コンポーネントからも値が読み取れる。 - コンテクストの値を提供するには子要素を
<MyContext.Provider value={...}>
でラップする。
- コンテクストは中間コンポーネントを貫通する。
- コンテクストを使えば、「周囲に適応する」コンポーネントが書ける。
- コンテクストを使用する前に、props を渡すか、
children
として JSX を渡す方法を検討してみる。
チャレンジ 1/1: props の穴掘り作業をコンテクストで置換
この例では、チェックボックスを切り替えることで、各 <PlaceImage>
に渡される imageSize
プロパティが変更されます。チェックボックスの state はトップレベルの App
コンポーネントで保持されていますが、各 <PlaceImage>
はそれを認識する必要があります。
現在、App
は imageSize
を List
に渡し、List
はそれを各 Place
に渡し、Place
はそれを PlaceImage
に渡しています。props から imageSize
を削除し、代わりにそれを App
コンポーネントから直接 PlaceImage
に渡すようにしてください。
コンテクストの宣言は Context.js
内で行えます。
import { useState } from 'react'; import { places } from './data.js'; import { getImageUrl } from './utils.js'; export default function App() { const [isLarge, setIsLarge] = useState(false); const imageSize = isLarge ? 150 : 100; return ( <> <label> <input type="checkbox" checked={isLarge} onChange={e => { setIsLarge(e.target.checked); }} /> Use large images </label> <hr /> <List imageSize={imageSize} /> </> ) } function List({ imageSize }) { const listItems = places.map(place => <li key={place.id}> <Place place={place} imageSize={imageSize} /> </li> ); return <ul>{listItems}</ul>; } function Place({ place, imageSize }) { return ( <> <PlaceImage place={place} imageSize={imageSize} /> <p> <b>{place.name}</b> {': ' + place.description} </p> </> ); } function PlaceImage({ place, imageSize }) { return ( <img src={getImageUrl(place)} alt={place.name} width={imageSize} height={imageSize} /> ); }