コンテクストで深くデータを受け渡す

通常、親コンポーネントから子コンポーネントには props を使って情報を渡します。しかし、props を多数の中間コンポーネントを経由して渡さないといけない場合や、アプリ内の多くのコンポーネントが同じ情報を必要とする場合、props の受け渡しは冗長で不便なものとなり得ます。コンテクスト (Context) を使用することで、親コンポーネントから props を明示的に渡さずとも、それ以下のツリー内の任意のコンポーネントが情報を受け取れるようにできます。

このページで学ぶこと

  • “props の穴掘り作業 (prop drilling)” とは何か
  • コンテクストを使って props の冗長な受け渡しを解消する方法
  • コンテクストの一般的な用途
  • コンテクストの代替手段

props の受け渡しに伴う問題

props の受け渡しは、UI ツリー内でデータを取り回してそれを必要とするコンポーネントに届けるための素晴らしい手法です。

しかし、props をツリー内の深い位置にまで渡す必要がある場合や、多くのコンポーネントが同じ props を必要としている場合、props の受け渡しは冗長で不便なものとなり得ます。最も近い共通の祖先要素は、データを必要としているコンポーネントから遠く離れているかもしれず、そのような高い位置まで state をリフトアップしていくと “props の穴掘り作業 (prop drilling)” と呼ばれる状況に陥ることがあります。

state のリフトアップ

3 コンポーネントからなるツリーを表した図。親には紫でハイライトされた値がある。その値は 2 つの子コンポーネント(紫)に渡されている。
3 コンポーネントからなるツリーを表した図。親には紫でハイライトされた値がある。その値は 2 つの子コンポーネント(紫)に渡されている。

props の穴掘り作業

10 ノードからなるツリーの図。各ノードには最大 2 つの子がある。ルートノードには紫でハイライトされた値がある。その値はまず 2 つの子に流れるが、いずれも値を下に流しているだけ。ルートの左の子は、2 つの子(紫)に値を渡している。ルートの右の子は、2 つの子のうち片方(紫)にのみ値を渡している。その子はさらに子に値を渡しているが、その子は値を下に流しているだけであり、さらにその子(紫)が値を受け取っている。
10 ノードからなるツリーの図。各ノードには最大 2 つの子がある。ルートノードには紫でハイライトされた値がある。その値はまず 2 つの子に流れるが、いずれも値を下に流しているだけ。ルートの左の子は、2 つの子(紫)に値を渡している。ルートの右の子は、2 つの子のうち片方(紫)にのみ値を渡している。その子はさらに子に値を渡しているが、その子は値を下に流しているだけであり、さらにその子(紫)が値を受け取っている。

もし 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> コンポーネントは、最も近い