React基礎講座 #12 useContext

読了 7分

前回は兄弟コンポーネントがデータを共有するために共通の親へ state をリフトアップするパターンを学びました。良いツールですが、コンポーネントツリーが深くなると一つ問題が起きます。今回はその問題と解決策である Context を扱っていきましょう。

Prop Drilling 問題 #

次のようなコンポーネントツリーを想像してみましょう。

コンポーネントツリー
App (state: user)
└── Layout
    └── Sidebar
        └── ProfileMenu
            └── UserAvatar (ここで user 情報が必要)

user state は App にあるのに、実際にその値を使うのは深いところにある UserAvatar です。間にある LayoutSidebarProfileMenu は user に関心がないにもかかわらず、ただ下に渡すために props を受け取らなければなりません。

<Layout user={user}>
  <Sidebar user={user}>
    <ProfileMenu user={user}>
      <UserAvatar user={user} />
    </ProfileMenu>
  </Sidebar>
</Layout>

このように 中間にあるコンポーネントたちが、自分と無関係な props を受け取ってただ下に渡すだけの状況prop drilling (プロップドリリング) と呼びます。深さが深くなったり、渡す値が多くなったりすると、コードが急速に汚くなっていきます。

React はこの問題を解くために Context API というツールを提供しています。

Context のアイデア #

Context の中核となるアイデアはシンプルです。

コンポーネントツリーのどこかでデータを「供給」しておけば、その下のどんな深さの子孫であっても直接「購読」して取り出して使える。

中間のコンポーネントを経由せず、上から下へデータが瞬間移動するわけですね。

Context 利用の3ステップ #

Context は次の3ステップで使います。

  1. Context を生成createContext で作る
  2. 供給<Context.Provider value={...}> でツリーのどこかを包みデータを提供
  3. 購読 — 子コンポーネントで useContext(Context) で値を取り出して使う

コードで見てみましょう。上の user の例を Context で書き直してみます。

ステップ1 — Context を生成 #

src/UserContext.js
import { createContext } from 'react';

export const UserContext = createContext(null);

createContext に入れる値はデフォルト値です。Provider が包んでいない位置で useContext を呼び出したときに使われる値です。

ステップ2 — Provider で供給 #

src/App.jsx
import { useState } from 'react';
import { UserContext } from './UserContext';
import Layout from './Layout';

function App() {
  const [user, setUser] = useState({ name: 'チョルス', email: 'cheolsu@example.com' });

  return (
    <UserContext.Provider value={user}>
      <Layout />
    </UserContext.Provider>
  );
}

export default App;

UserContext.Provider で囲んだ領域内のすべての子孫コンポーネントvalue の値を取り出して使えるようになります。中間のコンポーネントは、もう user を props として受け取る必要がありません。

src/Layout.jsx
import Sidebar from './Sidebar';

function Layout() {
  return (
    <div>
      <Sidebar />
    </div>
  );
}

export default Layout;
src/Sidebar.jsx
import ProfileMenu from './ProfileMenu';

function Sidebar() {
  return (
    <aside>
      <ProfileMenu />
    </aside>
  );
}

export default Sidebar;

LayoutSidebarProfileMenu は user について何も知る必要がありません。すっきりしましたね。

ステップ3 — useContext で購読 #

src/UserAvatar.jsx
import { useContext } from 'react';
import { UserContext } from './UserContext';

function UserAvatar() {
  const user = useContext(UserContext);

  if (!user) return <p>ログインが必要です</p>;

  return (
    <div>
      <p>{user.name}</p>
      <p>{user.email}</p>
    </div>
  );
}

export default UserAvatar;

useContext(UserContext) を呼び出すと、最も近い UserContext.Provider が提供した値をそのまま受け取れます。中間を経由せず、一度に取得したわけです。

値と関数を一緒に供給する #

Context の値はオブジェクトにして、state とその setter (または更新関数) を一緒に詰め込むパターンが非常によく使われます。そうすれば子孫で値を読むだけでなく、変更もできるようになりますね。

src/ThemeContext.js
import { createContext } from 'react';

export const ThemeContext = createContext({
  theme: 'light',
  toggleTheme: () => {},
});
src/App.jsx
import { useState, useCallback } from 'react';
import { ThemeContext } from './ThemeContext';
import Page from './Page';

function App() {
  const [theme, setTheme] = useState('light');

  const toggleTheme = useCallback(() => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light');
  }, []);

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      <Page />
    </ThemeContext.Provider>
  );
}
src/ThemeToggle.jsx
import { useContext } from 'react';
import { ThemeContext } from './ThemeContext';

function ThemeToggle() {
  const { theme, toggleTheme } = useContext(ThemeContext);

  return (
    <button onClick={toggleTheme}>
      現在のテーマ: {theme} (クリックして切り替え)
    </button>
  );
}

export default ThemeToggle;

子孫コンポーネントは theme (現在の値) と toggleTheme (変更関数) を一緒に取り出して使います。このパターンのおかげで、Context 一つで「共有 state とその操作方法」を一気に公開できます。

Provider をコンポーネントで包む #

Context の利用が増えてくると、Provider 自体を別のコンポーネントとして分離するのがすっきりします。状態管理ロジックを一箇所にまとめておく効果があります。

src/ThemeProvider.jsx
import { useState, useCallback } from 'react';
import { ThemeContext } from './ThemeContext';

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');

  const toggleTheme = useCallback(() => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light');
  }, []);

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

export default ThemeProvider;
src/App.jsx
import ThemeProvider from './ThemeProvider';
import Page from './Page';

function App() {
  return (
    <ThemeProvider>
      <Page />
    </ThemeProvider>
  );
}

App がぐっとシンプルになりました。テーマ関連のすべてのロジックが ThemeProvider の中にカプセル化され、別のところで持ってきて使うのも簡単になりました。

カスタムフックでもう一度包む #

useContext(ThemeContext) のように毎回 Context を直接 import するのも、ちょっと面倒です。よく使う Context はカスタムフックで包んで使い勝手を上げるパターンがよく使われます。

src/ThemeContext.js
import { createContext, useContext } from 'react';

export const ThemeContext = createContext({
  theme: 'light',
  toggleTheme: () => {},
});

export function useTheme() {
  return useContext(ThemeContext);
}
src/ThemeToggle.jsx
import { useTheme } from './ThemeContext';

function ThemeToggle() {
  const { theme, toggleTheme } = useTheme();
  // ...
}

消費する側のコードがずっと短くなります。カスタムフックについては次の記事 (#13) でより詳しく扱います。

Context を乱用しないでください #

Context は強力ですが、どこにでも使うツールではありません。 次のような点を覚えておきましょう。

1. 単純な prop の伝達ならただの props のほうが良い #

親子1〜2階層程度なら、props のほうがはるかに明示的で追跡しやすいです。深さが本当に深いとき (3〜4階層以上) や、複数の枝で一緒に使うデータのときに Context が活きます。

2. Provider の value が変わると、その下のすべての購読者が再レンダリングされる #

Context は子孫すべてを束ねてしまうため、value がよく変わると広範囲なリレンダリングが起きます。変更頻度が高いデータ (例: マウス座標) を Context で扱うと、パフォーマンス問題が発生する可能性があります。

3. Context はグローバル状態ライブラリではない #

Context は「データ伝達経路」であり、それ自体で精緻な状態管理ツールではありません。アプリ全体の複雑な状態 (グローバルなユーザー情報 + 通知 + カート + 設定 + …) を扱うなら、ZustandRedux ToolkitJotai のような専用ライブラリのほうが向いています。Context は小さな範囲の共有状態や、テーマ/言語/ユーザーといった「ほとんど変わらない」グローバルデータに適しています。

注記
「Context の適切な用途」を見極める一行: アプリ全般でよく使われるが、ほとんど変わらない値 (テーマ、ログイン中のユーザー、言語設定、トースト通知システムなど) が最もよく合います。

自分でやってみる #

テーマ (ライト/ダーク) を Context で管理し、2つの子コンポーネントが同じテーマ状態を共有する例を作ってみましょう。

src/ThemeContext.js:

src/ThemeContext.js
import { createContext, useContext, useState, useCallback } from 'react';

const ThemeContext = createContext({
  theme: 'light',
  toggleTheme: () => {},
});

export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');

  const toggleTheme = useCallback(() => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light');
  }, []);

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

export function useTheme() {
  return useContext(ThemeContext);
}

src/Card.jsx:

src/Card.jsx
import { useTheme } from './ThemeContext';

function Card({ children }) {
  const { theme } = useTheme();

  const styles = {
    background: theme === 'light' ? '#fff' : '#222',
    color: theme === 'light' ? '#000' : '#fff',
    padding: '16px',
    border: '1px solid #999',
    borderRadius: '8px',
    margin: '8px 0',
  };

  return <div style={styles}>{children}</div>;
}

export default Card;

src/ThemeToggle.jsx:

src/ThemeToggle.jsx
import { useTheme } from './ThemeContext';

function ThemeToggle() {
  const { theme, toggleTheme } = useTheme();

  return (
    <button onClick={toggleTheme}>
      現在: {theme === 'light' ? '☀ ライト' : '🌙 ダーク'} (クリックして切り替え)
    </button>
  );
}

export default ThemeToggle;

src/App.jsx:

src/App.jsx
import { ThemeProvider } from './ThemeContext';
import Card from './Card';
import ThemeToggle from './ThemeToggle';

function App() {
  return (
    <ThemeProvider>
      <ThemeToggle />
      <Card>
        <h2>1枚目のカード</h2>
        <p>テーマを変えると色が変わります</p>
      </Card>
      <Card>
        <h2>2枚目のカード</h2>
        <p>2枚のカードが同じテーマを共有しています</p>
      </Card>
    </ThemeProvider>
  );
}

export default App;

ボタンを押すと、2枚のカードの色が同時に変わります。CardThemeToggle は互いの存在を知らず、親も2つの間の props を仲介していないにもかかわらず、同じテーマ状態を共有しています。prop drilling なしに、ツリーのどこからでも同じデータにアクセスできるようになったのです。

まとめ #

今回の記事では prop drilling 問題と解決策である Context API を学びました。整理すると:

  • prop drilling — 中間のコンポーネントたちが、無関係な props をただ渡すだけの状況
  • Context はツリーのどこかにある値を、その下の子孫が直接取り出して使える「経路」
  • 利用の3ステップ: createContext<Provider value={...}>useContext
  • 値と setter をオブジェクトでまとめて供給するパターンがよく使われる
  • Provider のロジックは別コンポーネントに、消費はカスタムフックで包むときれい
  • 単純な prop の伝達はただの props のほうが良く、よく変わるデータには不適切

ここまでがバッチ2 (#9〜#12) の終わりです。フォーム、useEffect、stateのリフトアップ、Context まで扱ったので、これで小さな実戦アプリを最初から最後まで作るのに必要なパターンはほぼすべて揃いました。

次の記事「React基礎講座 #13 カスタムフック」では、コンポーネント間でロジックを共有する最もエレガントなツール、カスタムフックを扱っていきます。今回の記事で少し使った useTheme も、実はカスタムフックの一例だったのですね。次の記事で本格的に扱っていきましょう。

X