useContext超大全──React Context APIでグローバル状態をスマートに共有する

React

はじめに

React アプリが成長するにつれて、コンポーネント間で共通の値を渡す「props ドリリング」が増え、保守性が下がる課題に直面します。Context API と useContext フックは、この問題を標準機能だけで解決できる強力な仕組みです。本連載記事では公式ドキュメント https://react.dev/reference/react/useContext をベースに、基礎・設計・最適化・型安全・周辺ライブラリ まで 2 万文字超で徹底的に解説します。第一部では基本概念と最低限動くコード、スコープ設計、props ドリリングの解消フローをじっくり掘り下げます。

Context API の全体像

  • React.createContext で「箱」を作成
  • Provider が値を注入
  • useContext で最寄りの Provider から値を取得
  • Provider をネストすれば “局所グローバル” を作れる

図式化すると次のようになります。

<Context.Provider value={A}>
  └─ <ChildA />  ← useContext ⇒ A
      └─ <Context.Provider value={B}>
            └─ <ChildB />  ← useContext ⇒ B

外側と内側で値が上書きされるため、テーマやロケールなど「場面に応じて変わる設定」を自然に切り替えられます。

props ドリリング問題とContextの必要性

propsドリリングとは?

親→子→孫→ひ孫…と同じPropsをリレーして渡す状態。深いツリーでは可読性が悪化し、途中の中間コンポーネントが無関係のPropsを抱える。

Contextで解決

Providerをルート付近に置き、任意の深さで直接useContext。不要な中継Propsが消え、各コンポーネントが必要な値だけを購読できます。

具体例

function App() {
  const user = { id: 1, name: 'Taro' };
  return <Page user={user} />;
}
function Page({ user }) {             /* ← 中継だけ */
  return <Sidebar user={user} />;
}
function Sidebar({ user }) {          /* ← 中継だけ */
  return <UserProfile user={user} />;
}
function UserProfile({ user }) {
  return <p>{user.name}</p>;          /* ← 本当に必要なのはここだけ */
}

Context を導入すると、中継 Props をすべて削除し、下記のようにシンプル化できます。

const UserContext = createContext(null);
function App() {
  const user = { id: 1, name: 'Taro' };
  return (
    <UserContext.Provider value={user}>
      <Page />
    </UserContext.Provider>
  );
}
function UserProfile() {
  const user = useContext(UserContext);
  return <p>{user.name}</p>;
}

Context の作成と Provider 実装

createContext の引数

const ThemeContext = React.createContext('light'); // デフォルト値

1 引数めは フォールバック値。Provider がツリーになかった場合に限り返されるため、開発中の警告用や Storybook のダミー値として活用できます。

Provider に渡す value

<ThemeContext.Provider value="dark">
  <Toolbar />
</ThemeContext.Provider>

ポイント

  • Provider が再レンダーされると value の参照比較で下位ツリー全部が再レンダー
  • value がオブジェクトなら useMemo で安定化必須
const memoValue = useMemo(() => ({ theme, toggle }), [theme]);

useContext の基本挙動

const value = useContext(ThemeContext);
  • 最も近い Provider の value を返す
  • Provider が見つからない場合は createContext 時のデフォルト値
  • 複数回呼んでも React がキャッシュするためコストは低い

注意: useContext は必ず React のレンダーフェーズ内で呼ぶ。コールバックや条件分岐で早期 return する位置に入れるとルール違反。

スコープ設計:いつ Provider を分割すべきか

ケース1 Provider複数 Provider
テーマ、ロケールなど不変に近い設定
頻繁に変わる値(カーソル座標等)
複数ドメインデータ(Auth と Cart)

複数の値が同時に更新されるわけではない場合、それぞれ独立した Provider に分けることで再レンダー範囲を局所化できます。

サンプル:Todo アプリでの Context 分離

1. アプリ共通設定

const SettingsContext = createContext({
  showCompleted: true,
  toggleShow: () => {}
});

2. ログインユーザー

const AuthContext = createContext({
  user: null,
  login: () => {},
  logout: () => {}
});

3. Todo リスト

todo はサイズが大きく頻繁に変わるため、Context より useReducer + Context の複合パターンが適切です(第二部で詳細解説)。

パフォーマンス最適化と再レンダー制御

Context API は便利ですが、Provider の value が変わるたびに配下すべての useContext 呼び出しが再レンダー されます。大規模ツリーでは深刻なパフォーマンス低下につながるため、次の 4 つの対策を組み合わせましょう。

1. useMemo で value オブジェクトを安定化

const SettingsProvider = ({ children }) => {
  const [showCompleted, setShowCompleted] = useState(true);
  const ctx = useMemo(
    () => ({ showCompleted, toggle: () => setShowCompleted(v => !v) }),
    [showCompleted]    // プリミティブのみ依存
  );
  return <SettingsContext.Provider value={ctx}>{children}</SettingsContext.Provider>;
};

オブジェクト内の参照が変わらなければ再レンダーは下位に波及しません。

2. Context を分割する

更新頻度 が異なる値を 1 つの Provider に詰め込むと、滅多に変わらない設定が毎回レンダーのトリガーになります。

  • AuthContextThemeContext を個別 Provider にする
  • パフォーマンスクリティカルな配列や Map は専用 Context へ切り出す

3. Context Selector パターン

ライブラリ use-context-selector を使うと、特定プロパティだけを購読 でき、不要な再レンダーを劇的に減らせます。

import { createContext, useContextSelector } from 'use-context-selector';

const CountContext = createContext({ count: 0, inc: () => {} });

function CounterLabel() {
  const count = useContextSelector(CountContext, v => v.count);
  return <span>{count}</span>;
}

Provider の値がオブジェクト再生成されても count が変わらない限り CounterLabel は再レンダーされません。

4. memo + useContext は要注意

React.memo でラップしても、Context 更新は強制的に渡ってくるため 再レンダー抑止はできません。必要に応じ「コンテナ–プレゼンテーション分割」を行い、Context を読む部分をコンテナ、表示だけ行う部分を memo 化すると効果的です。

useReducer と Context を組み合わせたグローバルストア

小規模アプリなら Redux などを導入せずに、useReducer + Context で十分なグローバルストアを構築できます。

const TodoContext = createContext(null);

function todoReducer(state, action) {
  switch (action.type) {
    case 'add':
      return [...state, action.payload];
    case 'toggle':
      return state.map(t =>
        t.id === action.id ? { ...t, done: !t.done } : t
      );
    default:
      return state;
  }
}

export function TodoProvider({ children }) {
  const [todos, dispatch] = useReducer(todoReducer, []);
  const ctx = useMemo(() => ({ todos, dispatch }), [todos]);
  return <TodoContext.Provider value={ctx}>{children}</TodoContext.Provider>;
}

呼び出し側は dispatch({ type: 'add', payload }) で状態を更新し、todos を購読できます。Redux に比べてボイラープレートが少ないのが利点ですが、ミドルウェア機構開発ツール が欲しくなった時点で Redux Toolkit へ移行する判断基準となります。

型安全カスタムフックの作成(TypeScript)

export const SettingsContext =
  createContext<SettingsCtx | undefined>(undefined);

export function useSettings(): SettingsCtx {
  const ctx = useContext(SettingsContext);
  if (!ctx) {
    throw new Error('useSettings must be inside <SettingsProvider>');
  }
  return ctx;
}
  • undefined をコンテキスト型に含め、Provider 外使用をコンパイル時に検出
  • 呼び出し側は const { theme } = useSettings(); とシンプル

React 18 コンカレント特有の落とし穴

並列レンダーでは 「旧ツリー」と「新ツリー」が同時に存在 します。Context の値もレンダー単位でスナップショットされるため、更新が重なった場合は “数フレームだけ古いテーマで描画” のような一貫性揺らぎが起こり得ます。
対策:

  1. Transition 内で Provider を更新し、旧ツリーが素早く置き換わるようにする
  2. 非同期値(サーバフェッチ結果など)useSyncExternalStore で同期を取る

Suspense for Data Fetching と Context

React 18 の サスペンス対応データフェッチライブラリ(TanStack Query, SWR v2 等)は、Context でキャッシュを共有する設計が主流です。

<QueryClientProvider client={client}>  // Provider
  <App />
</QueryClientProvider>

内部的に Provider の値が変わらないよう慎重に実装されているため、大量のデータを扱っても再レンダーが抑えられます。

テストと Storybook パターン

単体テスト

  • Provider をラップしたテストユーティリティを作成
  • renderHook でカスタムフックのみをテスト
const wrapper = ({ children }) => <AuthProvider>{children}</AuthProvider>;
const { result } = renderHook(() => useAuth(), { wrapper });

Storybook

  • decorators で Provider を追加し、Context に基づく UI を再現
  • 複数バリエーションを Template で切り替え、locale や theme を擬似表示

よくあるエラーと解決策

エラー原因対処
Cannot read property 'xyz' of nullProvider がツリーに含まれていないカスタムフックでundefinedチェック
遅延評価したい値が毎回変わるProvider の value を毎レンダー生成useMemo で固定 or 値を分割Providerへ
子が不必要に再レンダーvalue がオブジェクトリテラルuseMemo + useCallback で参照安定化

まとめ

Context の基礎とスコープ設計、パフォーマンス最適化と useReducer 連携・型安全化・並列レンダーの留意点まで解説しました。ここまでを理解すれば、中小規模の React アプリなら Redux 等を導入せずとも十分スケーラブルな状態共有が可能です。

コメント