React

React Contextの使い方|createContext・useContextの実装とReact 19の新記法

React Contextの使い方|createContext・useContextの実装とReact 19の新記法

React Context(Context API)は、propsを何段も中継する配線を省いて、ツリーの下にあるコンポーネントから直接同じ値を読めるようにする仕組みです。使い始めはcreateContextとuseContextの2つで足りますが、その形のまま育てると、値を1つ書き換えただけで画面の広い範囲が再レンダーされる構造になります。React 19(19.0.0は2024年12月5日公開、最新安定版は2026年7月公開の19.2.8)ではプロバイダの書き方が増え、useの追加で読み取り方も変わりました。現行APIでの実装手順と、再レンダーを止める分割の型、Next.js App Routerでの制約までをまとめます。

まとめ

Contextは値を配る仕組みであって、値を保持する仕組みではありません。保持するのはuseStateやuseReducerです。この線引きを外すと、後から効く再レンダー問題を抱え込みます。

  • 宣言はcreateContext、配布は<Context>(React 19以降)、読み取りはuseContext。この3点で最小構成は完結します
  • <Context.Provider>も現行版で動きますが、React公式は将来のバージョンで非推奨にする予定と明言しています
  • useはifやループの内側でもコンテクストを読めます。トップレベル固定のuseContextとの実務上の差はここです
  • 再レンダーが広がる主因は、valueに毎レンダー新しいオブジェクトを渡すこと。useMemoとContextの分割で止めます
  • Server ComponentはContextを読めません。'use client'を付けたProviderコンポーネントでchildrenを包みます
  • 入力中の文字のような高頻度の更新と、サーバから取得したデータはContextに載せません

以下では、Contextの担当範囲、現行APIでの実装手順、再レンダーを抑える分割、Next.js App Routerでの配置、外部ストアへ切り替える判断基準を順に扱います。

React Contextが引き受ける配線と、状態の保持との役割分担

propsのバケツリレーを断つ値の配布

テーマ、ロケール、ログイン中のユーザー。こうした値は画面の深い位置で必要になる一方、途中のコンポーネントはそれを使いません。propsで渡すと、中継するだけの引数がレイアウトやリストに増えていきます。Contextは、この中継を省いて配布元と読み取り先を直接つなぐAPIです(公式の日本語訳は「コンテクスト」、日本語圏では「コンテキスト」とも表記されます)。

逆に言えば、親子2〜3段で済む値をContextに載せる理由はありません。propsで渡せる距離のものをContextにすると、値の出どころがコードから追えなくなるだけです。判断の目安は、中継専用のpropsが3段以上続くか、つまり値を使わないのに引数を足すファイルが3つを超えるかどうかです。

useStateによる保持とContextによる配布の分担

createContextが受け取る引数は既定値ですが、これは動的な状態ではありません。React公式のcreateContextリファレンスは「デフォルト値は『最後の手段』として使われるように意図されています。これは静的な値であり、時間が経過しても変化しません」と書いています。既定値が返るのは、ツリーの上側に一致するプロバイダが1つも存在しない場合だけです。

そのため、値を更新できるようにするには、Providerを置く側のコンポーネントでuseStateやuseReducerを宣言し、その現在値をProviderへ渡します。Contextは配送経路、stateは保管庫という分担になります。

createContext・Provider・useContextでの実装手順(React 19対応)

createContextの宣言と既定値の決め方

宣言は1ファイルに切り出します。コンポーネントと同じファイルにContextを同居させると、Fast Refreshがコンポーネント以外のexportを検知してモジュールを丸ごと作り直し、編集のたびにProvider配下のstateが失われるためです。TypeScriptで書くなら、既定値にundefinedを入れて読み取り用のカスタムフックで例外を投げる形が扱いやすいはずです。既定値にダミーのオブジェクトを置くと、Providerを付け忘れたまま画面が動いてしまい、原因の特定が遅れます。

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

export type Theme = 'light' | 'dark';

// Providerを必ず置く設計なら、既定値はundefinedにして
// Providerの外で読んだ事故を実行時に落とす
export const ThemeContext = createContext<Theme | undefined>(undefined);

export function useTheme(): Theme {
  const theme = useContext(ThemeContext);
  if (theme === undefined) {
    throw new Error('useTheme は <ThemeContext> の内側で呼んでください');
  }
  return theme;
}

既定値を意味のある値にしてよいのは、Providerを置かない使い方を正式にサポートするときだけです。テーマのようにフォールバックが自然な値なら'light'を既定値にしても構いません。

Providerの配置とContext直接レンダーへの移行

React 19以降、プロバイダは<ThemeContext.Provider>ではなく<ThemeContext>と書けます。公式のcreateContextリファレンスは「React 19 以降では、<SomeContext> 自体をプロバイダとしてレンダーできます」「以前のバージョンでは、代わりに <SomeContext.Provider> を使用してください」と、バージョンで書き分けを示しています。

// src/App.tsx
import { useState } from 'react';
import { ThemeContext, type Theme } from './contexts/ThemeContext';
import { Toolbar } from './Toolbar';

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

  return (
    // React 19以降は <ThemeContext.Provider> を書かずに済む
    <ThemeContext value={theme}>
      <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
        テーマ切り替え
      </button>
      <Toolbar />
    </ThemeContext>
  );
}

既存コードの.ProviderもReact 19系でそのまま動きます。ただしReact 19のリリース記事に「将来のバージョンでは <Context.Provider> を非推奨にする予定です」とあり、放置しても移行コストが後ろにずれるだけです。一括置換にはreactjs/react-codemodのremove-context-providerが使えます。README記載の実行形はnpx codemod react/19/remove-context-provider --target <path>です。

useContextとuseの使い分け

読み取りの既定はuseContextです。Providerより下にあるコンポーネントで呼べば、最も近いプロバイダのvalueが返ります。

// src/Toolbar.tsx
import { useTheme } from './contexts/ThemeContext';

export function Toolbar() {
  // useContext を包んだカスタムフック経由で読む
  const theme = useTheme();
  return <div className={theme}>現在のテーマ: {theme}</div>;
}

React 19で追加されたuseも、同じようにコンテクストを読めます。違いは呼べる位置です。公式のuseリファレンスは「useContext はコンポーネントのトップレベルで呼び出す必要がありますが、use は if や for などの条件式の中でも呼び出すことができます」と説明しています。表示モードによって読むコンテクストを変える、早期リターンの後で初めて読む、といった書き方が可能になります。

import { use } from 'react';
import { ThemeContext } from './contexts/ThemeContext';

// FullPanel はアプリ側で定義済みとする
function Panel({ compact }: { compact: boolean }) {
  if (compact) {
    // useContext はトップレベル固定。use は分岐の内側でも呼べる
    const theme = use(ThemeContext);
    return <span className={theme}>簡易表示</span>;
  }
  return <FullPanel />;
}

ただしuseにも制約があり、公式リファレンスは「use は try-catch ブロック内で呼び出すことはできません」と明記しています。条件分岐の必要がないならuseContextのままで構いません。useはPromiseを読んでSuspenseと組み合わせる用途が本体で、コンテクスト読み取りはその一部です。非同期レンダーまわりの前提はフロントエンド開発者が理解すべきAsync Reactの技術的背景と全体像で整理しています。

値の更新で再レンダーが広がる条件と、Context分割による抑制

毎レンダーで参照が変わるvalueへのオブジェクト直書き

公式のuseContextリファレンスは、再レンダーの条件を「あるコンテクストのプロバイダが異なる value を受け取ると、当該プロバイダより下にありそのコンテクストを使用しているすべての子コンポーネントは、React によって自動的に再レンダーされます」と定めています。判定は参照の比較です。value={{ currentUser, login }}のようにオブジェクトを直書きすると、中身が同じでもレンダーのたびに別オブジェクトになり、条件を毎回満たしてしまいます。

対策は、関数をuseCallbackで、オブジェクトをuseMemoで固定することです。公式も同じ形を推奨しています。

// src/contexts/AuthProvider.tsx
import { createContext, useCallback, useMemo, useState } from 'react';
import type { ReactNode } from 'react';

// User と LoginResponse はアプリ側の型定義とする
type AuthValue = {
  currentUser: User | null;
  login: (response: LoginResponse) => void;
};

export const AuthContext = createContext<AuthValue | undefined>(undefined);

export function AuthProvider({ children }: { children: ReactNode }) {
  const [currentUser, setCurrentUser] = useState<User | null>(null);

  const login = useCallback((response: LoginResponse) => {
    setCurrentUser(response.user);
  }, []);

  // useMemo が無いと、AuthProvider が再レンダーされるたびに
  // value が別オブジェクトになり全 Consumer が再レンダーされる
  const value = useMemo(() => ({ currentUser, login }), [currentUser, login]);

  return <AuthContext value={value}>{children}</AuthContext>;
}

この形にすると、currentUserが変わらない限りvalueの参照は保たれ、コンテクスト経由でのConsumerの再レンダーは起きません。なおReact Compiler(babel-plugin-react-compilerの1.0.0が2025年10月7日にnpmで公開)を有効にした環境では、このuseMemoとuseCallbackはコンパイラが挿入するため手書きが不要になります。導入していないプロジェクトでは手動のままです。

値用と更新関数用へのContext二分

useMemoだけでは足りない場面があります。値と更新関数を1つのオブジェクトで配ると、値が変わるたびに更新関数しか使わないコンポーネントまで再レンダーされるためです。ボタンだけを描画するコンポーネントが、カウントの表示更新に巻き込まれる形になります。

そこで、読み取り用と更新用を別々のContextに分けます。useStateが返すセッターはレンダーをまたいで同じ関数なので、更新用Contextのvalueは初回から変化しません。

// src/contexts/CountContext.tsx
import { createContext, useState } from 'react';
import type { Dispatch, ReactNode, SetStateAction } from 'react';

export const CountContext = createContext(0);
export const CountDispatchContext =
  createContext<Dispatch<SetStateAction<number>>>(() => {});

export function CountProvider({ children }: { children: ReactNode }) {
  const [count, setCount] = useState(0);

  // setCount は useState が同一の関数を返し続けるため、
  // 更新関数だけを読むコンポーネントは count が変わっても再レンダーされない
  return (
    <CountContext value={count}>
      <CountDispatchContext value={setCount}>
        {children}
      </CountDispatchContext>
    </CountContext>
  );
}

状態が配列やオブジェクトで、更新の種類が増えてきたらuseReducerへ寄せます。dispatchも同一の関数が返り続けるため、分割の効き方は同じです。

// src/contexts/TodoContext.tsx
// todoReducer・Todo・TodoAction はアプリ側の定義とする
import { createContext, useReducer } from 'react';
import type { Dispatch, ReactNode } from 'react';

export const TodoContext = createContext<Todo[]>([]);
export const TodoDispatchContext = createContext<Dispatch<TodoAction>>(() => {});

export function TodoProvider({ children }: { children: ReactNode }) {
  // dispatch も useReducer が同じ関数を返し続けるため参照が安定する
  const [todos, dispatch] = useReducer(todoReducer, []);

  return (
    <TodoContext value={todos}>
      <TodoDispatchContext value={dispatch}>{children}</TodoDispatchContext>
    </TodoContext>
  );
}

分割の判断は、読み手のコンポーネントが実際に何を購読しているかで決めます。値と更新関数を両方使うコンポーネントしか無いなら、分ける意味はありません。

childrenを受け取る形のProviderコンポーネント

もう1つ効くのが、stateを持つ層を切り出してchildrenをpropsで受け取る形です。stateを持つコンポーネントの内側に子要素を直書きすると、更新のたびにその関数が再実行され、子のReact要素も作り直されます。childrenとして外から渡せば、要素は前回と同じ参照のまま届き、Reactは再レンダーを省けます。

// 悪い例: state を持つ層に子要素を直書きする
function AppBad() {
  const [count] = useState(0);
  return (
    <CountContext value={count}>
      <Header />
      <Sidebar />
    </CountContext>
  );
}

// 良い例: children を props で受け取る層に分ける
function CountProvider({ children }: { children: ReactNode }) {
  const [count] = useState(0);
  return <CountContext value={count}>{children}</CountContext>;
}

function AppGood() {
  return (
    <CountProvider>
      <Header />
      <Sidebar />
    </CountProvider>
  );
}

ただしこの形でも、コンテクストを読んでいる子はvalueが変われば再レンダーされます。止められるのは、コンテクストを読んでいない中間コンポーネントや兄弟コンポーネントの巻き込みです。この書き分けはContext固有の話ではなく、コンポーネント合成の基本形でもあります。考え方の背景は「Composition Is All You Need」: Reactにおけるコンポジション思想の紹介と意義の解説にまとめています。

Next.js App RouterでのProvider配置とServer Componentの制約

‘use client’を付けたProvider専用コンポーネントの作り方

App Routerでは、layoutもpageも既定でServer Componentです。Next.js公式ドキュメント(Server and Client Components、バージョン16.2.12・2026年6月23日更新)は “React context is not supported in Server Components.” と明記しており、Server Componentの中でcreateContextやuseContextを呼ぶとエラーになります。同社のエラーページ「createContext in a Server Component」も “You are using createContext in a Server Component but it only works in Client Components.” と説明し、対処として該当ファイル先頭への'use client'追加を挙げています。

そこで、Provider専用のClient Componentを作ります。ファイル先頭に'use client'を置き、childrenを受け取って包む形です。

// app/providers.tsx
'use client';

import { createContext, useState } from 'react';
import type { ReactNode } from 'react';

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

export function Providers({ children }: { children: ReactNode }) {
  const [theme] = useState<Theme>('light');
  return <ThemeContext value={theme}>{children}</ThemeContext>;
}

これをServer Componentであるlayoutから読み込みます。Providerだけがクライアント側に入り、childrenに渡ったServer Componentはサーバでレンダーされた結果として流れ込むため、ページ全体がクライアントコンポーネント化することはありません。

// app/layout.tsx (Server Component のまま)
import { Providers } from './providers';
import type { ReactNode } from 'react';

export default function RootLayout({ children }: { children: ReactNode }) {
  return (
    <html lang="ja">
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  );
}

ルートlayoutとセグメントlayoutでのProvider配置の分け方

置き場所については、同ドキュメントが “You should render providers as deep as possible in the tree” と述べています。<html>ごと包まず{children}だけを包むのはこのためです。ルート直下に何でも積むと、静的に扱えるServer Componentの範囲が削られていきます。

テーマやロケールのように全画面で必要なものはルートlayout、管理画面でしか使わないものはapp/admin/layout.tsxのようなセグメントのlayout、と階層を分けてください。セグメント側に置けば、そのセグメントを開かないユーザーにProviderのJavaScriptが配信されません。次期16.3系の変更点はNext.js 16.3とは?Turbopack永続キャッシュとInstant Navigationsなど新機能を実装者目線で解説で扱っています。

Contextで押し切らず外部ストアへ切り替える判断基準

Contextはグローバル状態管理の代替品ではありません。購読の粒度を持たないため、「どのコンポーネントがどのフィールドを見ているか」に応じた部分更新ができないからです。分割とmemo化はその弱点を手作業で埋める作業であり、状態の種類が増えるほどProviderの入れ子と分割数が膨らみます。

状態の性質別に見た置き場所の対応表

状態の性質 具体例 置き場所
更新頻度が低く全画面で共有 テーマ、ロケール、ログインユーザー Context
サーバ由来でキャッシュと再取得が要る 一覧・詳細の取得結果 TanStack Query
1操作ごとに更新される 入力中の文字、ドラッグ位置 各コンポーネントのstate
共有範囲が広く購読粒度が要る 業務アプリの共有ストア Redux Toolkit、Zustand

この表で迷うのは3行目と4行目の境目です。共有したい相手が同じ画面の中に収まるならローカルstateとpropsで足り、画面をまたいで参照されるようになった時点で外部ストアを検討します。

Contextに載せてはいけない状態(フォーム入力とサーバ由来データ)

採用すべきでない場面を先に決めておくと迷いません。フォームの入力値をContextに載せる設計は避けてください。1文字ごとにProvider配下が再評価され、Contextを分割してもキー入力のたびに再レンダーが走ります。入力中はローカルstateで持ち、確定後の値だけを共有します。

サーバから取得したデータをContextに詰め込む設計も同じく避けます。必要になるのは配布ではなく、キャッシュ・再検証・重複リクエストの排除だからです。この領域は専用ライブラリの担当で、比較はTanStack Queryとは?React Queryとの違い・v5の使い方と脆弱性対策にまとめています。クライアント状態が業務ロジックを持ち始めたら、Redux Toolkitとは?createSliceとRTK Queryの実装と採用判断【2026年8月版】で扱うようなストアへ移す方が、Providerを分割し続けるより保守しやすくなります。

よくある質問

useContextとuseはどちらを使うべきですか?

既定はuseContextです。useを選ぶのは、条件分岐やループの内側でコンテクストを読む必要があるときに限ります。公式のuseリファレンスが示すとおり、両者の差はフックのルールに縛られるかどうかで、返る値は同じです。useはReact 19(2024年12月5日公開)以降のAPIのため、18系以前のプロジェクトでは使えません。

Context.ProviderやContext.Consumerはもう書けなくなりますか?

2026年8月時点の最新安定版である19.2.8を含め、現行のReact 19系ではどちらも動きます。ただし公式リファレンスはSomeContext.Providerを「React 19より前に値を配るための旧来の方法」と位置づけ、Context.Consumerにも「Legacy way (not recommended)」と注記して、新しく書くコードではuseContextを使うよう求めています。React 19のリリース記事も「将来のバージョンでは <Context.Provider> を非推奨にする予定です」と予告済みです。Context APIを新規に書くならcreateContextと<Context>、読み取りはuseContextが既定です。既存コードはreactjs/react-codemodのremove-context-providerで機械的に変換できます。

createContextの既定値には何を渡すべきですか?

Providerを必ず置く設計ならundefinedを渡し、読み取り用カスタムフックで例外を投げる形が扱いやすくなります。Providerが無くても成立する値(テーマの'light'など)だけを既定値にしてください。既定値は静的で、時間が経っても変化しません。更新可能な値にしたい場合はProvider側でstateを持ちます。

Contextを使うとアプリ全体が再レンダーされますか?

再レンダーされるのは、そのコンテクストを読んでいるProvider配下のコンポーネントだけです。読んでいないコンポーネントまでは波及しません。全体が再レンダーされているように見える場合、valueに毎レンダー新しいオブジェクトを渡している、あるいはstateを持つコンポーネントに子要素を直書きしている、のどちらかが原因になっているはずです。useMemoでの固定、Contextの分割、childrenを受け取る形への変更で切り分けてください。

Server ComponentでuseContextを呼べますか?

呼べません。Next.js公式ドキュメントが “React context is not supported in Server Components.” と明記しています。'use client'を付けたProviderコンポーネントを作り、Server Componentであるlayoutからそれをレンダーしてchildrenを包んでください。Providerはできるだけツリーの深い位置に置くよう公式が推奨しています。

関連記事

お気に入りに入れた記事の一覧

この記事は以下の記事からリンクされています

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2026.07.21 テックブログ Apache Tomcatの脆弱性一覧【2026年10月】最新版11.0.27・10.1.61・9.0.123とCVE-2026-59083ほかの影響確認・対応手順
  2. 2026.10.21 テックブログ NEC委託先の不正アクセスとは|顧客システム暗号化・情報漏えいの経緯と委託元が点検する正規アカウント
  3. 2026.10.21 テックブログ ふるさとチョイス不正アクセスで445万件流出:対象確認・パスワード変更と再開時期
  4. 2026.10.22 テックブログ Tomcatバージョン一覧とサポート期限【2026年10月】9.0系は2027年3月末で終了・9.1系と11.0系への移行判断
  5. 2024.07.05 コラム XAMPPの脆弱性とは?危険性とCVE-2024-4577への対策をわかりやすく解説【2026年10月】

RELATED POSTS 関連記事

目次