Context

React のコンテキストは、propsを使わずに子コンポーネントに親のデータを渡す為に使います。これにより

  • 複数のコンポーネントから成り立つコンポーネント間のpropsが簡単になる
  • 子が特定の親の下に置かれてるどうか確認できる

などの利点があります。

コンテキストを作るにはReact.createContextを使います。これはデフォルトで渡されることになる値を引数として1つ受け取ります。

const Meta = React.createContext({title: 'blog title'});

このMetaはオブジェクトでProviderConsumer2つのコンポーネントを持っています。
まずProviderは渡すデータを置くために使います。データはこの要素のvalue属性に渡します。 次にConsumerは渡ってきた値を受け取る為の要素です。
Consumerは上位階層にProviderがある時、一番近いそれに渡された値を受け取りますが、もしProviderが1つも無い場合、コンテキストを作る時に渡したデフォルト値を受け取ります。注意なのがこのコンポーネントのchildrenはデータを引数に受け取る関数だという点です。

です。この例はProviderと通す時と通さない時の動作を見れるもので、それぞれpropsで何も値を渡していないのにapp titleblog titleと別の内容が表示されます。

const Meta = React.createContext({ title: "blog title" });

const Title = () => {
  return (
    <Meta.Consumer>
      {meta => {
        return <h1>{meta.title}</h1>;
      }}
    </Meta.Consumer>
  );
};

const App = () => {
  return (
    <>
      <Title />
      <Meta.Provider value={{ title: "app title" }}>
        <Title />
      </Meta.Provider>
    </>
  );
}

useContext

Consumerの代わりに、useContextフックを使う方法もあります。このフックを使って上記のコードを書き換えるとこうなります。

const Meta = React.createContext({ title: "blog title" });

const Title = () => {
  const meta = React.useContext(Meta);

  return <h1>{meta.title}</h1>;
};

export default function App() {
  return (
    <>
      <Title />
      <Meta.Provider value={{ title: "app title" }}>
        <Title />
      </Meta.Provider>
    </>
  );
}

useContextへはConsumerを渡すわけではなくコンテキストそのものを渡します。

考察

memo化されたコンポーネントの中でuseContextを使っていると、Contextの値を更新してもそのコンポーネントは再レンダリングされません。このことからContextの全ての値を更新することはオススメしないです。

僕的にはContextへは何かキーの下にぶら下がれるオブジェクト値などを格納すると良いと思います。例えば以下のような感じです。

const contextValues = {
  '/': {
    title: 'Top Page',
    description: '...'
  },
  '/foo': {
    title: 'Foo Page',
    description: '...'
  },
}

そしてこのキー(path)だけ Flux で管理して必要に応じてuseContextを使っているコンポーネントに渡します。コンポーネントの中では渡されたpathを使いuseContextから値を取り出して使います。

const {title, description} = useContext(XxContext)[path];

このようにすることでmemo化されていても値を更新できますし、その第2引数を渡してない場合は、デフォルトで行われる浅い比較の回数も減らせれます。