Contextをおさらいする

2021.06.22

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

React では、コンポーネント間の状態は props として親コンポーネントから子コンポーネントへ渡す必要がありますが、複数の親子関係にいないコンポーネントを跨いで共通で利用したい値、例えばロケールの情報や UI のテーマ、ログイン状態など、がある場合、Context を使うことで面倒なバケツリレーをすることなくグローバルな値としてコンポーネント間で情報を共有できます。

Context の使い方

Context を利用するときは Context Provider で値を受け取り、その配下のコンポーネント内で変更を購読することができるようになります。

React には useContext という フックが用意されており、Context Provider 配下のコンポーネントからuseContext(<コンテキスト名>)でコンテキストの値を購読することができます。

import React, { createContext, useState } from 'react'
import { MembersCard } from './components/MembersCard'

// CustomThemeContextというContextを作成
export const CustomThemeContext = createContext<{
  setTheme: Dispatch<SetStateAction<string>>
}>({ setTheme: () => {} })

function App() {
  const [theme, setTheme] = useState<string>('light')
  return (
      <CustomThemeContext.Provider value={{ setTheme }}>
        <div className="App">
          <MembersCard /> {/** <-CustomThemeContextの値を講読できるよ */}
        </div>
      </CustomThemeContext.Provider>
  )
}

export default App

useContext を呼び出すコンポーネントはコンテキストが更新されるたびに再レンダーされるので、闇雲に利用するとコンポーネントの再利用性が落ちるので注意が必要です。

闇雲に利用してあまりよくなかったケース

実際の案件で以下のような要件がありました。

  • Global メニューの開閉状態を常に保持したい
  • ページ遷移をした後も遷移前のページの UI 状態を維持したい

全ページ共通のグローバルメニューの開閉状態がページ遷移の度にリセットされてしまうので、Context に持たせることで状態の維持に対応しました。

しかし、これだと この Context を参照している他のコンポーネント、つまりグローバルメニューを表示しているページ全部が Context の値が更新されるたびに再レンダーされてしまいます。

議論の末、上記の理由から UI の状態を Context に持つのは良くないよね、という結論に至りました。

Context を使った方がいい時

Context や状態管理ライブラリを使わない場合、コンポーネント間のデータの連携には props のバケツリレーが必須になります。

バケツリレーをするにはコンポーネント同士が親子関係にある必要がありますが、props で値を受け取りたいためだけに親子関係を作ることはコンポーネント同士の密結合に繋がるため、コンポーネントの再利用性が低下します。同じように親子関係を構築し、必要な値を props で渡してあげる必要があるからです。

このようなケースには Context もしくは Redux 等の状態管理ライブラリを利用するのが良いと考えます。

Context を使わない方がいい時

先ほどの Global メニューの状態を Context で管理した失敗例がわかりやすいですが、Context を更新したときに再レンダーされるべきでないコンポーネントにも再レンダーが走る場合は Context で管理すべきではないと言えます。

公式ドキュメントには Context を使った方が良いのは"何らかのデータが、ネストレベルの異なる多くのコンポーネントからアクセスできる必要がある時"と明記されており、このケースに当てはまるのは Theme の設定やロケールの情報などごくわずかだと考えます。

参考