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

useContextって結局どう使うのが正解?」「Reduxを入れるほどじゃないけど、propsをバケツリレーするのも限界…」――Reactで中規模アプリを書き始めた瞬間、誰もが直面する悩みです。本記事はuseContext 使い方React Context 状態管理を軸に、createContextContext.ProvideruseContextの基本から、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に渡された値をそのまま下流に流すパイプにすぎません。状態はuseStateuseReducerで管理し、その結果を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は値を配信するだけなので、状態のロジックは別途useStateuseReducerで持たせる必要があります。特にアクション種類が多い場合は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は依然として大規模アプリ・厳密なアクション履歴・タイムトラベルデバッグが必要な現場で第一候補です。createSlicecreateAsyncThunkRTK 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

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

コメント

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