Contextによるデータの深い受け渡し
通常、親コンポーネントから子コンポーネントへ情報を渡すにはpropsを使用します。しかし、propsを渡す方法は、中間に多くのコンポーネントを経由する必要がある場合や、アプリ内の多くのコンポーネントが同じ情報を必要とする場合、冗長で不便になる可能性があります。Contextを使用すると、親コンポーネントは、その下のツリー内の任意のコンポーネント(どれだけ深くても)に対して、propsを明示的に渡すことなく情報を利用可能にすることができます。
学習内容
- 「プロップドリリング」とは何か
- 繰り返しのprops受け渡しをcontextで置き換える方法
- contextの一般的なユースケース
- contextの一般的な代替手段
props受け渡しの問題点
propsの受け渡しは、UIツリーを通じてデータを明示的に、それを利用するコンポーネントへ導く優れた方法です。
しかし、あるpropsをツリーの深くまで渡す必要がある場合や、多くのコンポーネントが同じpropsを必要とする場合、propsの受け渡しは冗長で不便になる可能性があります。最も近い共通の祖先は、データを必要とするコンポーネントから遠く離れている可能性があり、状態のリフトアップをそこまで高く行うと、「プロップドリリング」と呼ばれる状況を引き起こす可能性があります。
状態のリフトアップ


プロップドリリング


propsを渡すことなく、ツリー内でデータを必要とするコンポーネントにデータを「テレポート」させる方法があれば素晴らしいと思いませんか?Reactのcontext機能を使えば、それが可能です!
Context: props受け渡しの代替手段
Contextを使用すると、親コンポーネントはその下のツリー全体にデータを提供できます。contextには多くの用途があります。ここでは一例を示します。サイズのHeadingコンポーネントを考えてみましょう。このコンポーネントは、そのサイズのためのlevelを受け入れます:
同じSection内の複数の見出しが常に同じサイズを持つようにしたいとします:
現在、各levelプロパティを個別に<Heading>に渡しています:
代わりにlevelプロパティを<Section>コンポーネントに渡し、<Heading>から削除できれば便利です。これにより、同じセクション内のすべての見出しが同じサイズになることを強制できます:
しかし、<Heading>コンポーネントは、最も近い<Section>のレベルをどうやって知ることができるでしょうか?これには、子コンポーネントがツリー上のどこかからデータを「要求」する方法が必要です。
プロパティだけではこれを行うことはできません。ここでコンテキストが役立ちます。次の3つのステップで行います:
- 作成コンテキストを作成します。(見出しレベルのため、
LevelContextと呼ぶことができます。) - 使用データを必要とするコンポーネントからそのコンテキストを使用します。(
HeadingはLevelContext) - 提供データを指定するコンポーネントからそのコンテキストを提供します。(
SectionはLevelContext)
コンテキストにより、親(遠く離れた親でも!)がその内部のツリー全体にデータを提供できます。
近い子孫でのコンテキストの使用


遠い子孫でのコンテキストの使用


ステップ1:コンテキストを作成する
まず、コンテキストを作成する必要があります。ファイルからエクスポートする必要があります。これにより、コンポーネントがそれを使用できるようになります:
createContextへの唯一の引数は、createContextのデフォルト値です。ここでは、1は最大の見出しレベルを指しますが、任意の種類の値(オブジェクトでも)を渡すことができます。デフォルト値の重要性は次のステップで確認します。
ステップ2: コンテキストを使用する
ReactからuseContextフックとあなたのコンテキストをインポートします:
現在、Headingコンポーネントはpropsからlevelを読み取っています:
代わりに、levelプロップを削除し、インポートしたばかりのコンテキストLevelContextから値を読み取ります:
useContextはフックです。useStateやuseReducerと同様に、フックはReactコンポーネントの内部(ループや条件分岐の内部ではなく)で直ちに呼び出す必要があります。useContextは、HeadingコンポーネントがLevelContextを読み取りたいことをReactに伝えます。
これで、Headingコンポーネントにはlevelプロップがなくなったので、以下のようにJSXでHeadingにlevelプロップを渡す必要はありません:
代わりに、Sectionがそれを受け取るようにJSXを更新します:
念のため、動作させようとしていたマークアップは以下の通りです:
この例はまだ完全には動作しません!すべての見出しが同じサイズになっています。なぜなら、コンテキストを使用しているにもかかわらず、まだそれを提供していないからです。Reactはどこからそれを取得すればよいかわからないのです!
コンテキストを提供しない場合、Reactは前のステップで指定したデフォルト値を使用します。この例では、1をcreateContextの引数として指定したので、useContext(LevelContext)は1を返し、すべての見出しを<h1>に設定してしまいます。この問題を解決するために、各Sectionが独自のコンテキストを提供するようにしましょう。
ステップ3: コンテキストを提供する
現在、Sectionコンポーネントはその子要素をレンダリングしています:
コンテキストプロバイダーでそれらをラップし、LevelContextを提供します:
これはReactに対して、「この<Section>内の任意のコンポーネントがLevelContextを要求した場合、このlevelを渡せ」と指示します。コンポーネントは、UIツリー内の上方にある最も近い<LevelContext>の値を使用します。
これは元のコードと同じ結果ですが、各Headingコンポーネントにlevelプロップを渡す必要はありませんでした!代わりに、見出しレベルを「把握する」ために、上方にある最も近いSectionに問い合わせています:
- あなたは
levelプロップを<Section>に渡します。 Sectionはその子要素を<LevelContext value={level}>でラップします。Headingは、LevelContextの最も近い上位の値をuseContext(LevelContext)を使って取得します。
同じコンポーネントからコンテキストを使用および提供する
現在のところ、各セクションのlevelを手動で指定する必要があります:
コンテキストを使用すると上位のコンポーネントから情報を読み取ることができるため、各Sectionは上位のSectionからlevelを読み取り、level + 1を自動的に渡すことができます。その方法は以下の通りです:
この変更により、levelプロップをどちらにも、つまり<Section>にも<Heading>にも渡す必要がなくなります:
これで、HeadingとSectionの両方がLevelContextを読み取って、自分がどれだけ「深い」位置にあるかを判断します。そして、Sectionはその子要素をLevelContextでラップし、その内部にあるものはすべて「より深い」レベルにあることを指定します。
注記
この例では、ネストされたコンポーネントがコンテキストをどのように上書きできるかを視覚的に示すために見出しレベルを使用しています。しかし、コンテキストは他の多くのユースケースでも有用です。サブツリー全体が必要とする任意の情報を渡すことができます:現在のカラーテーマ、現在ログインしているユーザーなどです。
コンテキストは中間コンポーネントを通過する
コンテキストを提供するコンポーネントとそれを使用するコンポーネントの間に、好きなだけ多くのコンポーネントを挿入できます。これには、<div>のような組み込みコンポーネントや、自分で構築したコンポーネントも含まれます。
この例では、同じPostコンポーネント(破線の境界線を持つ)が2つの異なるネストレベルでレンダリングされています。その内部の<Heading>が、最も近い<Section>から自動的にレベルを取得することに注目してください:
これが動作するために特別なことは何もしていません。Sectionはその内部のツリーに対するコンテキストを指定するため、<Heading>をどこに挿入しても、正しいサイズで表示されます。上のサンドボックスで試してみてください!
コンテキストを使用すると、コンポーネントが「周囲の環境に適応」し、どこで(言い換えれば、どのコンテキスト内で)レンダリングされるかに応じて異なる表示を行うコンポーネントを記述できます。
コンテキストの仕組みは、CSSプロパティの継承を思い起こさせるかもしれません。CSSでは、color: blueを<div>に指定すると、その内部のDOMノードは、中間にある別のDOMノードがcolor: greenで上書きしない限り、深さに関係なくその色を継承します。同様に、Reactでは、上から来るコンテキストを上書きする唯一の方法は、子要素を異なる値を持つコンテキストプロバイダーでラップすることです。
CSSでは、colorやbackground-colorのような異なるプロパティは互いに上書きしません。<div>のcolorを赤に設定しても、background-colorには影響しません。同様に、異なるReactコンテキストも互いに上書きしません。createContext()で作成する各コンテキストは、他のコンテキストから完全に独立しており、その特定のコンテキストを使用および提供するコンポーネントを結び付けます。1つのコンポーネントが問題なく多くの異なるコンテキストを使用または提供することができます。
コンテキストを使用する前に
コンテキストは非常に魅力的ですが、これは同時に過剰使用しやすいことも意味します。いくつかの階層を超えてpropsを渡す必要があるからといって、その情報をコンテキストに入れるべきではありません。
コンテキストを使用する前に検討すべき代替案をいくつか紹介します:
- まずはpropsを渡すことから始めましょう。コンポーネントが単純でない場合、十数個のpropsを十数個のコンポーネントを通して渡すことは珍しくありません。面倒に感じるかもしれませんが、どのコンポーネントがどのデータを使用するかが非常に明確になります!コードを保守する人は、propsでデータフローを明示的にしたことを喜ぶでしょう。
- コンポーネントを抽出し、子要素としてJSXを渡します。データを使用しない(単にさらに下に渡すだけの)多くの中間コンポーネント層を通してデータを渡す場合、これはしばしば、途中でいくつかのコンポーネントを抽出するのを忘れたことを意味します。例えば、
postsのようなデータpropsを、<Layout posts={posts} />のように直接使用しない視覚的コンポーネントに渡しているかもしれません。代わりに、Layoutがchildrenをpropとして受け取り、<Layout><Posts posts={posts} /></Layout>をレンダリングするようにします。これにより、データを指定するコンポーネントとそれを必要とするコンポーネントの間の層の数が減ります。
これらのアプローチのどちらもうまくいかない場合は、コンテキストを検討してください。
コンテキストのユースケース
- テーマ設定:アプリがユーザーに外観の変更(例:ダークモード)を許可する場合、アプリの最上位にコンテキストプロバイダーを配置し、視覚的な見た目を調整する必要があるコンポーネントでそのコンテキストを使用できます。
- 現在のアカウント:多くのコンポーネントが現在ログインしているユーザーを知る必要があるかもしれません。コンテキストに置くことで、ツリー内のどこでも読み取ることが便利になります。一部のアプリでは、複数のアカウントを同時に操作することもできます(例:別のユーザーとしてコメントを残す)。そのような場合、UIの一部を異なる現在のアカウント値を持つネストされたプロバイダーでラップすると便利です。
- ルーティング:ほとんどのルーティングソリューションは、内部で現在のルートを保持するためにコンテキストを使用します。これが、すべてのリンクが自分がアクティブかどうかを「知る」方法です。独自のルーターを構築する場合、これも行いたいかもしれません。
- 状態管理:アプリが成長するにつれて、アプリの最上位に近い多くの状態を持つことになるかもしれません。その下にある多くの離れたコンポーネントがそれを変更したいと思うかもしれません。複雑な状態を管理し、面倒な作業なしに離れたコンポーネントに渡すために、リデューサーとコンテキストを一緒に使用することは一般的です。
コンテキストは静的な値に限定されません。次のレンダリングで異なる値を渡すと、Reactはその下にあるすべての読み取りコンポーネントを更新します!これが、コンテキストが状態と組み合わせて使用されることが多い理由です。
一般的に、ツリーの異なる部分にある離れたコンポーネントに情報が必要な場合、コンテキストが役立つ良い兆候です。
まとめ
- コンテキストは、コンポーネントがその下のツリー全体に情報を提供できるようにします。
- コンテキストを渡すには:
-
export const MyContext = createContext(defaultValue)で作成してエクスポートします。 -
useContext(MyContext)フックに渡して、どの子コンポーネントでも(深さに関係なく)読み取ります。 - 子要素を
<MyContext value={...}>でラップして、親から提供します。
-
- コンテキストは中間のコンポーネントを通過します。
- コンテキストにより、「周囲の環境に適応する」コンポーネントを記述できます。
- コンテキストを使用する前に、propsを渡すか、JSXを
childrenとして渡してみてください。
いくつかの課題を試してみましょう
Challenge 1 of 1:プロップドリリングをコンテキストに置き換える #
この例では、チェックボックスを切り替えると、各 <PlaceImage> に渡される imageSize プロップが変更されます。チェックボックスの状態は最上位の App コンポーネントで保持されていますが、各 <PlaceImage> はそれを認識する必要があります。
現在、 App は imageSize を List に渡し、 List はそれを各 Place に渡し、 Place はそれを PlaceImage に渡しています。 imageSize プロップを削除し、代わりに App コンポーネントから直接 PlaceImage に渡すようにしてください。
コンテキストは Context.js で宣言できます。
