useContext完全ガイド〜Context API・状態管理・パフォーマンス〜【2026年版】

「useContextって結局どう使うのが正解?」「Reduxを入れるほどじゃないけど、propsをバケツリレーするのも限界…」――Reactで中規模アプリを書き始めた瞬間、誰もが直面する悩みです。本記事はuseContext 使い方とReact Context 状態管理を軸に、createContext・Context.Provider・useContextの基本から、Provider分割・Selectorパターン・useSyncExternalStoreによる再レンダリング抑制、さらにRedux Toolkit・Zustand・Jotai・Recoil・Valtio・TanStack Queryへの移行判断までを、サンプルコード20本超・比較表3つ・FAQ7問で徹底解説します。読み終えるころには、「Contextだけで戦える領域」と「ライブラリを入れるべき領域」の境界が腹落ちし、現場でレビュー時に堂々と判断を下せるようになっているはずです。

  1. useContextはなぜ必要なのか
    1. props drillingの典型的な構造
    2. useContextが向いている用途・向いていない用途
    3. Reactの公式ドキュメントで明示されている位置づけ
  2. createContextとuseContextの基本構文
    1. 最小サンプル: テーマカラーの配信
    2. createContextの引数とデフォルト値の役割
    3. Providerのvalueに渡せるもの
    4. カスタムフックでラップする定石
  3. useReducerと組み合わせた本格的な状態管理
    1. カート状態をuseReducer + Contextで管理する
    2. stateとdispatchを別Contextに分けるメリット
    3. 初期値を遅延評価する
  4. Contextの再レンダリング問題と解決策
    1. なぜReact.memoでは止まらないのか
    2. 解決策1: Provider valueをuseMemoで固定する
    3. 解決策2: Providerを目的別に分割する
    4. 解決策3: Selectorパターン(use-context-selector)
    5. 解決策4: useSyncExternalStoreで外部ストア化する
  5. useContext vs Redux/Zustand/Jotai 比較
    1. 主要状態管理ライブラリの比較表
    2. Reduxを今選ぶべきケース
    3. Zustandがハマるケース
    4. Jotai・Recoil・Valtioの位置づけ
    5. サーバー状態はTanStack Queryに任せる
  6. Provider分割と設計パターン
    1. パターン1: State/Dispatch分離Provider
    2. パターン2: Composition Provider
    3. パターン3: スコープ付きProvider
    4. パターン4: グローバルvsローカルの判断軸
    5. Provider設計のチェックリスト
  7. パフォーマンス最適化の実践
    1. React DevTools Profilerで何を見るか
    2. React.memoとuseMemoの併用パターン
    3. Contextの分割でN+1問題を避ける
    4. 計測サンプル: Profilerで効果を確認
    5. 2026年: React Compilerとの関係
  8. useContextからライブラリへの移行判断
    1. 移行を検討すべきサイン
    2. 段階的移行のすすめ
    3. 移行先選定フロー
    4. 「Contextでよかった」典型例
  9. useContextでハマりやすい落とし穴
    1. 落とし穴1: Provider valueにオブジェクトリテラルを直書き
    2. 落とし穴2: Context値をimportして直接使う
    3. 落とし穴3: Provider未配置時のサイレント失敗
    4. 落とし穴4: SSR/Server Componentsでの誤用
    5. 落とし穴5: depsを忘れたuseEffectとContextの組み合わせ
  10. useContextに関するよくある質問(FAQ)
    1. Q1: useContextとReduxはどちらを覚えるべき?
    2. Q2: Context Providerは何個までネストしてOK?
    3. Q3: useContextは遅い?
    4. Q4: Server Componentsの時代にContextは不要になる?
    5. Q5: useContext + useReducerはReduxの代わりになる?
    6. Q6: Context.Consumer はもう使わない?
    7. Q7: Contextは何個まで作ってOK?
  11. まとめ: useContextを正しく使い、必要なら卒業する

useContextはなぜ必要なのか

Reactのコンポーネントツリーは、propsを上から下へ受け渡すことで状態を共有します。しかしツリーが深くなるほど「中継するだけのコンポーネント」が増え、いわゆるprops drilling(バケツリレー)が発生します。useContextは、この問題を解決するためにReactが標準で提供しているツリー横断の値配信機構です。Reduxのような外部ライブラリではなく、Reactそのものの一機能である点が重要です。

props drillingの典型的な構造

たとえば、最上位のAppで取得したログインユーザー情報を、5階層下のUserAvatarに渡したい場合、本来関係のない中間コンポーネントすべてにuser propsを書く羽目になります。Contextを使えば、Providerで一度配信した値を、必要なコンポーネントがuseContextで直接受け取れます。

// drillingあり: 中間コンポーネントが全てuserを中継
<App user={user}>
  <Layout user={user}>
    <Header user={user}>
      <Nav user={user}>
        <UserAvatar user={user} />
      </Nav>
    </Header>
  </Layout>
</App>

useContextが向いている用途・向いていない用途

Contextは万能ではありません。「めったに変わらない値・ツリー全体で共有する値」に向いており、「秒間に何度も変わる値・大量のコンポーネントが個別に購読する値」には向きません。後者はContext単体ではパフォーマンス問題を引き起こすため、後述のSelectorパターンや外部ストアと組み合わせます。

Reactの公式ドキュメントで明示されている位置づけ

React公式はuseContextを「状態管理ライブラリではなく、値を配信する仕組み」と明確に位置づけています。つまりContextそれ自体は状態を持たず、Providerに渡された値をそのまま下流に流すパイプにすぎません。状態はuseStateやuseReducerで管理し、その結果をContextで配信する、というのが正しい設計です。

createContextとuseContextの基本構文

まずは最小構成でContextを動かしてみましょう。createContextでコンテキストオブジェクトを作り、Context.Providerで値を配信し、useContextで受け取る、という3点セットが基本です。

最小サンプル: テーマカラーの配信

import { createContext, useContext, useState } from "react";

// 1. Contextオブジェクトの生成(デフォルト値を指定)
const ThemeContext = createContext("light");

function App() {
  const [theme, setTheme] = useState("dark");
  // 2. Providerで値を配信
  return (
    <ThemeContext.Provider value={theme}>
      <Page />
    </ThemeContext.Provider>
  );
}

function Page() {
  // 3. useContextで受け取る
  const theme = useContext(ThemeContext);
  return <div className={theme}>現在のテーマ: {theme}</div>;
}

createContextの引数とデフォルト値の役割

createContext(defaultValue)のdefaultValueは、ProviderでラップされていないコンポーネントがuseContextを呼んだときに返される値です。テストや単体描画で便利ですが、本番ではProviderで必ずラップする運用が安全です。

Providerのvalueに渡せるもの

Providerのvalueには、プリミティブ・オブジェクト・関数・配列など任意のJavaScript値を渡せます。ただし毎レンダリングで新しいオブジェクトを生成すると、すべての購読コンポーネントが再レンダリングされるため、後述のuseMemoでの参照固定が重要になります。

カスタムフックでラップする定石

useContextをコンポーネント内で直接呼ぶのではなく、useThemeのようなカスタムフックでラップするのが定番です。Provider未配置時のエラーチェックも入れられ、利用側のコードも読みやすくなります。

function useTheme() {
  const ctx = useContext(ThemeContext);
  if (ctx === null) {
    throw new Error("useTheme must be used within ThemeProvider");
  }
  return ctx;
}

useReducerと組み合わせた本格的な状態管理

Contextは値を配信するだけなので、状態のロジックは別途useStateやuseReducerで持たせる必要があります。特にアクション種類が多い場合はuseReducer + Contextの組み合わせが「ミニReduxパターン」として定着しており、外部ライブラリなしで複雑な状態を扱えます。

カート状態をuseReducer + Contextで管理する

import { createContext, useContext, useReducer } from "react";

const CartStateContext = createContext(null);
const CartDispatchContext = createContext(null);

function cartReducer(state, action) {
  switch (action.type) {
    case "add":
      return { ...state, items: [...state.items, action.item] };
    case "remove":
      return {
        ...state,
        items: state.items.filter((i) => i.id !== action.id),
      };
    case "clear":
      return { ...state, items: [] };
    default:
      return state;
  }
}

export function CartProvider({ children }) {
  const [state, dispatch] = useReducer(cartReducer, { items: [] });
  return (
    <CartStateContext.Provider value={state}>
      <CartDispatchContext.Provider value={dispatch}>
        {children}
      </CartDispatchContext.Provider>
    </CartStateContext.Provider>
  );
}

export const useCartState = () => useContext(CartStateContext);
export const useCartDispatch = () => useContext(CartDispatchContext);

stateとdispatchを別Contextに分けるメリット

上記のようにstateとdispatchを別々のContextで配信すると、dispatchしか使わないコンポーネント(例: 「追加」ボタン)はuseCartDispatchのみ購読し、state変更による再レンダリングの影響を受けません。これは後述する「Context再レンダリング問題」への第一の対策です。

初期値を遅延評価する

useReducerの第3引数に初期化関数を渡せば、初期値の計算を1回だけに抑えられます。LocalStorageからの復元など、コストの高い初期化に有効です。

const [state, dispatch] = useReducer(
  cartReducer,
  null,
  () => {
    const saved = localStorage.getItem("cart");
    return saved ? JSON.parse(saved) : { items: [] };
  }
);

Contextの再レンダリング問題と解決策

Contextを使い始めると、ほぼ確実にぶつかるのが「Providerの値が変わると、useContextしている全てのコンポーネントが再レンダリングされる」問題です。これはReactの仕様であり、React.memoでは止められません。原因と対策を順に押さえます。

なぜReact.memoでは止まらないのか

React.memoはpropsの変化を比較して再レンダリングを抑制しますが、Contextの値変化は「内部の購読」によって伝わるため、propsが変わっていなくても再描画が走ります。「memoで囲めば安全」は誤りです。

解決策1: Provider valueをuseMemoで固定する

Providerのvalueにオブジェクトリテラルを直接渡すと、親の再レンダリングのたびに新しい参照が生まれ、配信される全コンポーネントが再描画されます。useMemoで参照を固定するのが第一歩です。

function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  // valueの参照をuserとsetUserが変わったときだけ更新
  const value = useMemo(() => ({ user, setUser }), [user]);
  return (
    <AuthContext.Provider value={value}>
      {children}
    </AuthContext.Provider>
  );
}

解決策2: Providerを目的別に分割する

1つの巨大Contextに全状態を詰めるのではなく、関心ごとに小さなProviderへ分割します。テーマ・認証・カート・通知などを別Contextにすれば、テーマ切替時に認証情報の購読者が巻き込まれることはありません。

// 悪い例: 全てを1つにまとめる
<AppContext.Provider value={{ theme, user, cart, notifications }}>...</AppContext.Provider>

// 良い例: 関心ごとに分割
<ThemeProvider>
  <AuthProvider>
    <CartProvider>
      <NotificationProvider>{children}</NotificationProvider>
    </CartProvider>
  </AuthProvider>
</ThemeProvider>

解決策3: Selectorパターン(use-context-selector)

Context全体ではなく、その中の特定フィールドだけを購読したい場合、use-context-selectorライブラリが定番です。Reactには公式のContext selectorは未実装で、コミュニティ実装で補う形になります。

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

const StoreContext = createContext({ count: 0, name: "" });

function CountDisplay() {
  // countが変わったときだけ再レンダリング
  const count = useContextSelector(StoreContext, (s) => s.count);
  return <p>{count}</p>;
}

解決策4: useSyncExternalStoreで外部ストア化する

React 18で追加されたuseSyncExternalStoreを使えば、Context経由でなく外部ストアを購読でき、フィールド単位の選択購読が標準APIで可能になります。ZustandやValtioが内部で使っているのもこのAPIです。

import { useSyncExternalStore } from "react";

function createStore(initial) {
  let state = initial;
  const listeners = new Set();
  return {
    getState: () => state,
    setState: (next) => {
      state = { ...state, ...next };
      listeners.forEach((l) => l());
    },
    subscribe: (l) => {
      listeners.add(l);
      return () => listeners.delete(l);
    },
  };
}

const store = createStore({ count: 0 });

function Counter() {
  const count = useSyncExternalStore(
    store.subscribe,
    () => store.getState().count
  );
  return <button onClick={() => store.setState({ count: count + 1 })}>{count}</button>;
}

useContext vs Redux/Zustand/Jotai 比較

「Contextでいいのか、ライブラリを入れるべきか」は永遠のテーマです。代表的な選択肢を実務観点で比較します。

主要状態管理ライブラリの比較表

観点 useContext Redux Toolkit Zustand Jotai
学習コスト 低 中〜高 低 低
バンドル増 0KB 約13KB 約1KB 約3KB
Provider必要 必須 必須 不要 推奨
Selector 標準なし あり(reselect) あり atom単位で自動
DevTools なし 公式強力 あり あり
非同期 手書き createAsyncThunk async関数で素直に atomWithSuspense等
想定規模 小〜中 中〜大 小〜大 中

Reduxを今選ぶべきケース

Redux Toolkitは依然として大規模アプリ・厳密なアクション履歴・タイムトラベルデバッグが必要な現場で第一候補です。createSlice・createAsyncThunk・RTK Queryが揃い、サーバー状態とクライアント状態を同じ思想で扱えます。

Zustandがハマるケース

Zustandは「Provider不要・hooksベース・selector標準装備」という三拍子で、中小規模〜中規模に最適です。useContextの薄い代替として導入でき、グローバルカウンタやモーダル制御程度なら数行で完結します。

import { create } from "zustand";

const useCart = create((set) => ({
  items: [],
  add: (item) => set((s) => ({ items: [...s.items, item] })),
  clear: () => set({ items: [] }),
}));

function CartCount() {
  // selectorで購読フィールドを絞る
  const count = useCart((s) => s.items.length);
  return <span>{count}件</span>;
}

Jotai・Recoil・Valtioの位置づけ

Jotaiは「atom」という極小単位で状態を持つ思想で、細粒度の購読を最も自然に書けます。Recoilは元Facebook発の同じ系統で、Jotaiの方が活発に保守されている印象です。ValtioはProxyベースで「ミューテーション風に書けるイミュータブル更新」が特徴で、フォーム状態と相性がよいです。

サーバー状態はTanStack Queryに任せる

覚えておきたいのは、APIから取ってくるデータは状態管理ライブラリで持たないのが2026年の主流という点です。TanStack Query(旧React Query)やSWRがキャッシュ・再検証・楽観的更新を一手に引き受けるため、Context/Zustand/Reduxには「クライアント固有のUI状態」だけを残すと設計がきれいに整います。

Provider分割と設計パターン

useContextで中規模アプリを構築する際、Provider設計の質がコードベースの寿命を左右します。ここでは実務で使えるパターンをまとめます。

パターン1: State/Dispatch分離Provider

前述のCartStateContext・CartDispatchContextのように、読み取りと更新を別Contextにする方式です。読み取り側は値変化で再レンダリングされますが、更新側(dispatch)は参照が安定するため不要な再描画を回避できます。

パターン2: Composition Provider

複数のProviderを1つのAppProviderに合成して、ルートをすっきり保つパターンです。テストやStorybookで一括ラップしやすくなります。

function AppProvider({ children }) {
  return (
    <QueryClientProvider client={queryClient}>
      <ThemeProvider>
        <AuthProvider>
          <CartProvider>{children}</CartProvider>
        </AuthProvider>
      </ThemeProvider>
    </QueryClientProvider>
  );
}

パターン3: スコープ付きProvider

同じContextを異なる値でツリーの一部だけ上書きするパターンです。ダッシュボード全体は「dark」、設定モーダル内だけ「light」など、ローカルなテーマ切替に有効です。

<ThemeContext.Provider value="dark">
  <Dashboard />
  <ThemeContext.Provider value="light">
    <SettingsModal />
  </ThemeContext.Provider>
</ThemeContext.Provider>

パターン4: グローバルvsローカルの判断軸

「Contextに載せるか、コンポーネント内のuseStateに留めるか」の判断はシンプルです。2階層以上下に渡したい、または横断的に複数ブランチで読みたい場合のみContextを検討します。それ以外はpropsで十分です。早すぎるContext化は、テスト容易性とコンポーネントの独立性を損ねます。

Provider設計のチェックリスト

  • ProviderのvalueはuseMemoで参照固定しているか
  • StateとDispatchは分離されているか
  • 1Providerに無関係な状態を詰め込んでいないか
  • カスタムフックでuseContextをラップし、Provider未配置時にエラーを投げているか
  • テスト用のラッパーProviderを用意しているか

パフォーマンス最適化の実践

Context関連のパフォーマンス問題は、計測なしの「念のためのmemo」では悪化することもあります。ここでは計測ベースで最適化する手順を示します。

React DevTools Profilerで何を見るか

Chrome拡張のReact Developer ToolsのProfilerタブで操作を録画すると、コミットごとに「どのコンポーネントが・なぜ再レンダリングされたか」が表示されます。原因列に「Context changed」と出ているコンポーネントが、Contextによる再描画の対象です。

React.memoとuseMemoの併用パターン

Context値を直接購読する末端コンポーネントは、それ自体は再描画されてOKです。しかしその下流の重いコンポーネントにはReact.memoをかけ、propsをuseMemoで安定化させることで、再描画の伝搬を止められます。詳細はuseMemo vs useCallback完全比較でも解説しています。

Contextの分割でN+1問題を避ける

1つのContextに大きなオブジェクトを載せると、どれか1フィールドの変更で全購読者が再描画されます。変更頻度の異なるフィールドを別Providerに分けるのが最も効果的な対策です。「秒間更新されるマウス座標」と「滅多に変わらないユーザー情報」を同じContextに入れてはいけません。

計測サンプル: Profilerで効果を確認

import { Profiler } from "react";

function onRender(id, phase, actualDuration) {
  console.log(`[${id}] ${phase} ${actualDuration.toFixed(2)}ms`);
}

<Profiler id="CartList" onRender={onRender}>
  <CartList />
</Profiler>

2026年: React Compilerとの関係

React 19系で正式リリースされたReact Compilerは、useMemo・useCallbackの手書きを大幅に削減してくれますが、Contextの再レンダリング問題そのものは解決しません。Provider分割やSelectorの設計判断は、依然として開発者の責任領域です。React Compilerの詳細はReact Hooks 完全実践ガイドも参照してください。

useContextからライブラリへの移行判断

「いつContextを卒業して状態管理ライブラリを入れるか」は、アプリの寿命に直結します。判断軸を整理します。

移行を検討すべきサイン

  1. Providerの数が10個を超え、ネストの管理が辛い
  2. 1つのContextに5つ以上のフィールドが乗り、再レンダリング制御が破綻している
  3. 非同期処理(API取得・楽観的更新)のコードがContext内に肥大化している
  4. 異なるブランチ間で状態を双方向に同期したいケースが頻出する
  5. テスト時のProviderラップが煩雑で、テストコードが読みづらい

段階的移行のすすめ

いきなり全Contextを置き換える必要はありません。サーバー状態をTanStack Queryへ、頻繁に変わるUI状態をZustandへと、影響範囲の小さい領域から切り出すのが安全です。「めったに変わらない設定値・ユーザー情報」はuseContextのまま残してOKです。

移行先選定フロー

状況 推奨される選択
サーバーAPIのデータが中心 TanStack Query / SWR
小〜中規模・hooks重視 Zustand
細粒度の購読が必要 Jotai
大規模・履歴管理が重要 Redux Toolkit
フォーム状態・ミューテーション中心 Valtio / React Hook Form
めったに変わらない設定 useContext維持

「Contextでよかった」典型例

  • テーマ(ダーク/ライト)切替
  • i18n(現在のロケール)
  • 認証ユーザー情報(ログイン直後に1回セットして、その後ほぼ不変)
  • 機能フラグ・実験グループ
  • ルーター(React Routerは内部的にContextを多用)

useContextでハマりやすい落とし穴

最後に、レビューでよく指摘される失敗パターンをまとめます。

落とし穴1: Provider valueにオブジェクトリテラルを直書き

value={{ user, login }}のように毎レンダリングで新オブジェクトを作ると、全購読者が再レンダリングされます。useMemoでの参照固定が必須です。

落とし穴2: Context値をimportして直接使う

Contextオブジェクト自体を共有モジュールからimportせず、Providerコンポーネントだけをexportする設計が安全です。誤って別の場所でuseContext(別のContext)されると、デフォルト値が返ってバグになります。

落とし穴3: Provider未配置時のサイレント失敗

カスタムフック化でエラーを投げる設計にしておかないと、Providerをラップし忘れたときにdefaultValueが静かに返り、原因不明のバグになります。

function useAuth() {
  const ctx = useContext(AuthContext);
  if (!ctx) throw new Error("useAuth must be used within AuthProvider");
  return ctx;
}

落とし穴4: SSR/Server Componentsでの誤用

Next.js App Routerなどでは、ContextはClient Componentでしか使えません。Server ComponentでuseContextを呼ぶとエラーになります。Provider側のファイルには必ず"use client"を記述します。

"use client";
import { createContext, useState } from "react";
const ThemeContext = createContext("light");
export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState("light");
  return <ThemeContext.Provider value={theme}>{children}</ThemeContext.Provider>;
}

落とし穴5: depsを忘れたuseEffectとContextの組み合わせ

useContextで取得した値をuseEffectのdepsから漏らすと、古い値を参照し続けるstale closure問題が起きます。depsには必ずContextから取り出した値を含めます。

useContextに関するよくある質問(FAQ)

Q1: useContextとReduxはどちらを覚えるべき?

まずuseContextを確実に理解してください。Reduxはその上位概念であり、Contextの限界を体感した後に学ぶと「なぜReduxが必要なのか」が腹落ちします。逆順で学ぶと、Reduxの設計思想が過剰に見えてしまいます。

Q2: Context Providerは何個までネストしてOK?

明確な上限はありませんが、7〜8個を超えると可読性が落ちるのが体感値です。Composition Providerでまとめるか、TanStack Query・Zustandなどに一部を移譲して数を減らしましょう。

Q3: useContextは遅い?

useContext自体は非常に軽量です。遅さの正体は「value変化で全購読者が再描画される」仕様であり、useContextというAPIそのものではありません。Provider分割とuseMemoで多くの問題は解決します。

Q4: Server Componentsの時代にContextは不要になる?

いいえ。Server Componentsで扱えるのは静的・サーバー側のデータで、クライアントのインタラクション状態(モーダル・テーマ・カート)はClient ComponentsとuseContextの領域として残り続けます。役割分担が明確になっただけです。

Q5: useContext + useReducerはReduxの代わりになる?

小〜中規模ならYesです。ただしミドルウェア・タイムトラベルデバッグ・selector最適化・公式DevToolsが必要になった瞬間にRedux Toolkitに軍配が上がります。境界は「複数開発者で長期保守する大規模アプリかどうか」です。

Q6: Context.Consumer はもう使わない?

関数コンポーネントが標準の現在、useContextで十分です。Context.Consumerはクラスコンポーネント時代の名残で、新規コードでの使用は推奨されません。

Q7: Contextは何個まで作ってOK?

用途別に小さく分けるのが原則なので、10〜20個程度なら全く問題ありません。むしろ「1つの神Context」より、目的別に分割された多数の小Contextの方が再レンダリング制御もテストも容易です。

まとめ: useContextを正しく使い、必要なら卒業する

useContextは「Reactが標準で持つツリー横断の値配信機構」であり、状態管理ライブラリそのものではありません。useStateやuseReducerと組み合わせ、Provider分割・useMemoでの参照固定・カスタムフックでのラップという3点セットで、中小規模アプリなら十分に戦えます。一方で、頻繁な更新・細粒度の購読・サーバー状態の管理が必要になった時点で、TanStack QueryやZustand、必要ならRedux Toolkitに段階的に委譲しましょう。「全部Context」も「最初からRedux」もアンチパターンです。アプリの規模と更新頻度を見極めて、適切なツールを適切な範囲で使い分けるのが2026年のReact状態管理の正解です。本記事と合わせて、React Hooks 完全実践ガイドとuseMemo vs useCallback完全比較も読むと、Hooks全体の地図が完成します。

タイトルとURLをコピーしました