Skip to content

4.4 Context — 使いどころと避けどころ ​

Part 3.5 で扱った props は、親から子への 明示的な データ伝達の手段でした。深い階層に値を渡したいだけなのに、途中のコンポーネント全部に props を書かされる状況 —— これを prop drilling(バケツリレー) と呼びます。Context は、この prop drilling を避けるための仕組みです。最初は「どこで使う / 使わない」の線引きで迷いやすいので、使いどころと避けどころを対にして整理していきましょう。

メンタルモデル: 祖先の「空気」を子孫が読む ​

Context は、祖先コンポーネントが提供(provide)した値を、子孫コンポーネントがどの階層にいても読み取れる 仕組みです。途中のコンポーネントはその値を知らなくて構いません。

Button は Page / Sidebar / Menu を経由せずに、useContext(ThemeContext) で直接 Root の値 "dark" を読めます。

触って比べる — prop drilling と Context ​

下のデモで階層の深さを変えたり、モードを切り替えたりして、両方の挙動を見比べてみてください。prop drilling では中間の Layer すべてに theme を渡さないといけませんが、Context では中間を素通りして末端の Button が直接値を読めることが伝わるはずです。

<App>  theme = "dark"
<Layer1>theme="dark"↓ props
<Layer2>theme="dark"↓ props
<Layer3>theme="dark"↓ props
<Button /> props 経由で受け取った theme = dark経路: App → L1 → L2 → L3 → Button

階層を深くすると、prop drilling モードでは全ての中間 Layer に theme を書き渡す必要があることが伝わります。 Context モードでは中間は素通りし、Button が直接値を読みます。

3 ステップで使う ​

Step 1: Context を作る ​

tsx
// theme-context.ts
import { createContext } from "react";

export type Theme = "light" | "dark";
export const ThemeContext = createContext<Theme>("light");

createContext(defaultValue) の引数は、Provider が上に 1 つもなかった場合に使われるフォールバック値 です(テストやコンポーネント単体表示用の安全装置として機能します)。

Step 2: Provider として値を提供する ​

React 19 では <Context> 自体を Provider として使える記法が入りました。

tsx
// React 19 での推奨形
import { useState } from "react";
import { ThemeContext, Theme } from "./theme-context";

export function App({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState<Theme>("dark");
  return (
    <ThemeContext value={theme}>
      {children}
    </ThemeContext>
  );
}

React 18 までは <ThemeContext.Provider value={theme}> と書く必要がありました。React 19 では <ThemeContext value={theme}> で同じ意味 になります。.Provider は将来的に非推奨になる予定です。

Step 3: 子孫で読む ​

tsx
import { useContext } from "react";
import { ThemeContext } from "./theme-context";

function Button() {
  const theme = useContext(ThemeContext);
  return <button data-theme={theme}>click</button>;
}

use(ThemeContext) でも同じように読めます。違いは、use なら 条件分岐や早期 return の後 でも呼べる点です(4.2 参照)。

図 4.4.1 Context の 3 ステップ ​

1. 作るcreateContext(default)チャンネルを定義するファイル: theme-context.ts2. 提供する<ThemeContext value=..>祖先で値を流すReact 19 では .Provider 省略可3. 読むuseContext / use子孫のどこからでも中間は素通りContext の使い方は、この 3 ステップだけ

典型的な使いどころ ​

公式ドキュメントが挙げている用途は次の通りです。

  • テーマ: ダーク / ライト切り替え
  • 認証ユーザー: ログインユーザー情報
  • ルーティング: 現在のルート情報(React Router などの内部実装で使われています)
  • i18n: 表示言語・翻訳テーブル

共通項は、「アプリ全体 / 大きな木で共有され、更新頻度が低く、読みたいコンポーネントが広く散らばっている値」です。

避けどころ ​

Context は強力ですが、すべての「値を共有したい場面」に向くわけではありません。公式ドキュメントでも、Context を使う前に次を検討するよう促しています。

  1. props で渡せないか: 深い階層に渡すのが面倒でも、「誰が何に依存しているか」がコードで明示されるのは大きな利点です。
  2. コンポーネント合成(composition)で回避できないか: 中間コンポーネントに children を取らせて、データを知っているコンポーネントを直接子として置く方法です。
    tsx
    // 避けたい: Layout に posts を渡して、中で Posts にバケツリレー
    <Layout posts={posts} />
    
    // 望ましい: Posts を直接 children として渡す
    <Layout>
      <Posts posts={posts} />
    </Layout>

Context に向かない値 ​

  • 頻繁に変わる値(秒単位で更新される時刻、アニメーションのフレームなど): Context の値が変わるたびに、useContext を呼んでいるすべての子孫が再レンダーされます。
  • 大きな配列 / 深いオブジェクト: 上記と同じ理由で、一部が変わっただけでも全コンシューマーが再レンダーされてしまいます。
  • 全く関係のない複数の値をまとめる: 1 つの Context に user / theme / locale を全部入れると、どれか 1 つの変更で全員が再レンダーされてしまいます。関心ごとに Context を分ける のが基本です。

再レンダーの仕組み ​

useContext(MyContext) を呼ぶコンポーネントは、MyContext の value が Object.is で変わった瞬間に再レンダー されます。途中のコンポーネントが memo されていても、これは止まりません。

tsx
// これは毎レンダーで新しいオブジェクトが作られてしまう
<AuthContext value={{ user, login }}>
  {children}
</AuthContext>

上記は毎レンダーで value の参照が変わるため、全コンシューマーが再レンダーされます。対策は useMemo で参照を安定させることです。

tsx
import { useMemo } from "react";

const authValue = useMemo(() => ({ user, login }), [user, login]);
<AuthContext value={authValue}>{children}</AuthContext>;

useReducer + Context の組み合わせ(軽量な状態管理代替) ​

中規模アプリで「Redux を入れるほどではないけれど、useState を各所で書くのは辛い」というときは、useReducer と Context を組み合わせた構成がよく使われます。公式ドキュメントの Scaling Up with Reducer and Context でも紹介されている手法です。

要点: state と dispatch を 別の Context に分ける ことで、dispatch しか使わないコンポーネントは state 変化で再レンダーされなくなります。

tsx
// tasks-context.tsx (構造のみ抜粋)
import { createContext, useContext, useReducer, ReactNode } from "react";

type Task = { id: string; title: string; done: boolean };
type Action =
  | { type: "add"; title: string }
  | { type: "toggle"; id: string }
  | { type: "remove"; id: string };

const TasksContext = createContext<Task[] | null>(null);
const TasksDispatchContext = createContext<React.Dispatch<Action> | null>(null);

function reducer(state: Task[], action: Action): Task[] {
  switch (action.type) {
    case "add":
      return [...state, { id: crypto.randomUUID(), title: action.title, done: false }];
    case "toggle":
      return state.map((t) => (t.id === action.id ? { ...t, done: !t.done } : t));
    case "remove":
      return state.filter((t) => t.id !== action.id);
  }
}

export function TasksProvider({ children }: { children: ReactNode }) {
  const [tasks, dispatch] = useReducer(reducer, []);
  return (
    <TasksContext value={tasks}>
      <TasksDispatchContext value={dispatch}>{children}</TasksDispatchContext>
    </TasksContext>
  );
}

export function useTasks() {
  const tasks = useContext(TasksContext);
  if (tasks === null) throw new Error("TasksProvider の外側です");
  return tasks;
}

export function useTasksDispatch() {
  const dispatch = useContext(TasksDispatchContext);
  if (dispatch === null) throw new Error("TasksProvider の外側です");
  return dispatch;
}

利用側はこうなります。

tsx
function AddTask() {
  const dispatch = useTasksDispatch(); // dispatch だけ欲しい
  return (
    <button onClick={() => dispatch({ type: "add", title: "Buy milk" })}>
      Add
    </button>
  );
}

function TaskList() {
  const tasks = useTasks(); // tasks だけ欲しい
  return (
    <ul>
      {tasks.map((t) => (
        <li key={t.id}>{t.title}</li>
      ))}
    </ul>
  );
}

図 4.4.2 Reducer + Context の分離設計 ​

state の変化は TaskList だけを再レンダーさせ、AddTask は静かに居られます。

Part 7 に進む前に ​

Context + Reducer で足りないケース(グローバルな読み取り頻度が高い、複雑な更新ロジックが絡む、デバッグツールが欲しい)では、Zustand / Redux Toolkit などを検討します。詳しくは 7.2 状態管理 で扱います。

参照ソース ​