React Contextは、親コンポーネントが持つ値を、間のコンポーネントにpropsを通さずに子孫へ届ける仕組みです。書くのはcreateContextで宣言し、<Context value={...}>で配り、useContextかuseで読む、の3手だけです。
難しいのは書き方より、値を変えたときにどこまで再レンダーが広がるかと、Next.jsのServer Componentとどう組み合わせるかです。この記事では、React 19.3.1で再レンダー回数を計測した結果と、Next.js 16.4.3で配置を試した結果を載せています。
まとめ
Contextは値を「配る」仕組みで、値を「持つ」のはuseStateやuseReducerです。再レンダーを調べるときは、valueの参照の変化に加え、Contextの分割範囲と、親から子へ進む通常の再レンダーも確認します。
- 最小構成は
createContext・<Context value>・useContextの3つ。<Context.Provider>も動きますが、React 19以降の新規コードでは<Context>で書きます valueにオブジェクトを直書きすると、親が無関係な理由で再レンダーしただけで読み手も再レンダーします(計測では3回の親更新で読み手が3回余分に描画)。useMemoで固定すると0回でしたmemoはpropsが変わらない場合の再レンダーを省略できますが、そのコンポーネント自身が読むContextの値の変更は止めません。値と更新関数を別のContextに分けると、更新関数の参照が同じ限り、値用Contextの変更による再レンダーは更新関数だけ読む側に伝わりません。ただし、親の再レンダーやprops・自身のstateの変更による再レンダーは別です- React Compiler 1.0を有効にすると、直書きの
valueも自動でメモ化され、無関係な再レンダーは読み手に届きませんでした - React 19.3以降は、
'use client'のモジュールで作ったContextをServer Componentから直接<ThemeContext value="dark">で描画できます。Server Componentの中で作る・読むことは今もできません - 入力中の文字のように高頻度で変わる値はローカルstateに、サーバーから取得したデータのキャッシュや再取得はデータ取得ライブラリに任せ、Contextは配る役に限ります
React Contextが担う値の配布と、状態の保持との分担
propsのバケツリレーを断つ値の配布
テーマ、ログイン中のユーザー、表示言語のように、離れた多数のコンポーネントが同じ値を読む場面では、値を使わない中間コンポーネントまでpropsを受け渡す「バケツリレー」が起きます。Contextを使うと、配る側と読む側だけが値に触れ、中間のコンポーネントはpropsを持たずに済みます。
ただしReact公式の学習ページは、Contextを使う前に「propsで渡す」「JSXをchildrenとして渡す」の2つを試すよう勧めています。2〜3階層のリレーなら、props渡しのほうがデータの流れを追いやすいからです。childrenで中間層を薄くする設計はReactのchildrenとpropsの違い|型定義・設計パターン・React 19の変更点で扱っています。
useStateによる保持とContextによる配布の分担
Contextそのものは値を保存しません。公式リファレンスも、コンテクストオブジェクトは「どのコンテクストか」を表すだけで情報を持たないと説明しています。値を変えたいときは、Providerを置くコンポーネントでuseStateやuseReducerを持ち、その値と更新関数をvalueに渡します。
つまり「グローバルな状態管理」と呼ばれる役割の半分はReactのstateが担い、Contextは配線だけを担います。この切り分けが、後の章で扱う再レンダー対策と、外部ストアへ移る判断の前提になります。
createContext・Provider・useContextで実装する手順(React 19.3対応)
createContextの宣言と既定値の決め方
Contextはコンポーネントの外で一度だけ作り、配る側と読む側の両方からimportできるようにファイルを分けます。
import { createContext } from 'react';
export const ThemeContext = createContext('light');
export const AuthContext = createContext(null);
引数は、上位に一致するProviderが無いときに返る既定値です。createContextのリファレンスは、既定値を「最後の手段」のフォールバックで、静的で時間とともに変わらないものと位置づけ、意味のある既定値が無ければnullを指定するよう書いています。テーマのように既定値で動いて困らない値は実値を、ログインユーザーのようにProvider無しで読まれたら不具合になる値はnullを渡し、読み取り側で検出します。
<Context>による値の配布とContext.Providerからの移行
React 19から、Context自体をProviderとして描画できます(React 19のリリース記事)。
import { useContext, useState } from 'react';
function Toolbar() {
const theme = useContext(ThemeContext);
return <p>現在のテーマ: {theme}</p>;
}
import { ThemeContext } from './ThemeContext';
export default function App() {
const [theme, setTheme] = useState('light');
return (
<ThemeContext value={theme}>
<Toolbar />
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
切り替え
</button>
</ThemeContext>
);
}
従来の<ThemeContext.Provider value={theme}>も19.3.1で動きます。19.3.1ではThemeContext.ProviderがThemeContextと同じオブジェクトを指していることも確認でき、書き換えなくても挙動は変わりません。一方でリリース記事は「将来のバージョンで<Context.Provider>を非推奨にする」と明記しているので、既存コードは公式のcodemodで置き換えられます。変換後は差分と動作を確認し、将来の非推奨化に備えます。
npx codemod react/19/remove-context-provider --target src
このcodemodはreactjs/react-codemodのremove-context-providerで、Context.Providerの開始タグと終了タグをContextに変換します。Context.Consumerはリファレンスで「Legacy way (not recommended)」とされており、新しく書く理由はありません。
useContextとuseによる値の読み取り
import { use, useContext } from 'react';
import { ThemeContext } from './ThemeContext';
function Button({ children }) {
const theme = useContext(ThemeContext);
return <button className={theme}>{children}</button>;
}
function Heading({ children, themed }) {
if (themed) {
const theme = use(ThemeContext);
return <h2 className={theme}>{children}</h2>;
}
return <h2>{children}</h2>;
}
どちらも、呼び出したコンポーネントから見て最も近い上位のProviderの値を返します。違いは呼べる場所で、useContextはフックの規則どおりトップレベル限定、useはifやループの内側でも呼べます。ただしuseのリファレンスは、Suspenseと連携するため内部で例外を投げる仕組みなのでtry〜catchの中では呼べないとしています。useのPromise側の使い方はReactのuseフック(use API)の使い方——React 19のPromise・Context読み取りを解説にまとめています。
同じコンポーネントの中では、自分が描画したProviderの値は読めません。useContextが見るのは呼び出し元より上のProviderだけなので、計測でも、外側の値outerを読んでouter>innerを配ったコンポーネント自身はouterを受け取りました。
読み取り用カスタムフックによるProvider外呼び出しの検出
既定値をnullにしたContextは、読む側で毎回null判定が要ります。判定をカスタムフックに閉じ込めると、Providerの置き忘れを例外で即座に見つけられます。
import { createContext, useContext } from 'react';
const AuthContext = createContext(null);
export function useAuth() {
const auth = useContext(AuthContext);
if (auth === null) {
throw new Error('useAuth は AuthProvider の内側で呼んでください');
}
return auth;
}
export function AuthProvider({ user, children }) {
return <AuthContext value={user}>{children}</AuthContext>;
}
Context本体は非公開にし、同じモジュールからProviderと読み取り用フックを公開すると、読み方を揃えられます。TypeScriptではcreateContext<AuthValue | null>(null)のように値の型を指定すると、null判定後にAuthValueを返せます。フックの命名や返り値の設計はReactカスタムフックのベストプラクティス|命名・返り値・メモ化の判断基準を参照してください。
Context更新による再レンダーの伝播条件と計測結果
ここからは、React 19.3.1とreact-dom 19.3.1をjsdom 30.1.3上で動かし、memoで包んだ読み手コンポーネントの描画回数を数えた結果です(開発ビルド・StrictModeなし。回数は初回マウントの1回を含みます)。読み手の関数本体でカウンタを1増やし、更新は1回ずつact()で確定させてから数えました。
| 条件 | 操作 | 読み手の描画回数 |
|---|---|---|
| valueにオブジェクト直書き | 親の無関係なstateを3回更新 | 4回 |
| valueをuseMemoで固定 | 親の無関係なstateを3回更新 | 1回 |
| memoした中間層の下で読む | Contextの値を3回更新 | 中間層1回・読み手4回 |
| 値と更新関数を別Contextに分割 | 値を3回更新 | 値の読み手4回・更新関数の読み手1回 |
| 直書き+React Compiler 1.0.0 | 親の無関係なstateを3回更新 | 1回 |
| 直書き+React Compiler 1.0.0 | 続けて値を3回更新 | 4回(Compilerなしは7回) |
毎レンダーで参照が変わるvalueへのオブジェクト直書き
Reactはvalueの新旧をObject.isで比べ、違えばそのContextを読む全コンポーネントを再レンダーします。value={{ user, setUser }}と書くと、親が描画されるたびに新しいオブジェクトが作られるため、中身が同じでも「変わった」と判定されます。表の1行目がこれで、親の無関係な更新3回がそのまま読み手に伝わりました。
function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const value = useMemo(() => ({ user, setUser }), [user]);
return <AuthContext value={value}>{children}</AuthContext>;
}
useMemoでuserが変わったときだけ新しいオブジェクトを作ると、2行目のとおり無関係な更新は届かなくなります。setUserはReactが同一性を保証するので依存配列に入れなくて構いません。関数を自作して渡すならuseCallbackで包みます。
memoはContextの伝播を止めないという前提
3行目のとおり、memoで包んだ中間コンポーネントは再レンダーしませんでしたが、その下でContextを読む子は値の更新ごとに描画されました。memoが比べるのはpropsだけで、Contextの変化は読み手へ直接届きます。重い子孫を守りたいなら、memoを足すより、その子孫がContextを読まない形に分けるほうが効きます。
値用と更新関数用へのContext二分
const CountContext = createContext(0);
const SetCountContext = createContext(() => {});
function CountProvider({ children }) {
const [count, setCount] = useState(0);
return (
<CountContext value={count}>
<SetCountContext value={setCount}>{children}</SetCountContext>
</CountContext>
);
}
ボタンのように更新するだけで値を表示しないコンポーネントはSetCountContextだけを読みます。4行目の計測では、値を3回変えても更新関数側の読み手は初回の1回しか描画されませんでした。値が複数あって頻度が違う場合(ユーザー情報は滅多に変わらず、通知件数は頻繁に変わる、など)も、頻度ごとにContextを分けると同じ効果が出ます。
React Compiler 1.0を有効にしたときの差
同じ直書きのコードをbabel-plugin-react-compiler 1.0.0で変換すると、Providerを描画するコンポーネントがメモ化され、無関係な更新では読み手が再描画されませんでした(表の5〜6行目)。このコードに限れば、useMemoを手で書かなくても1行目の余分な再レンダーは起きませんでした。
ただしCompilerは、コンポーネント外の変数への代入など規則に反するコードを黙って対象外にします。計測用のコードでも、外部変数へsetterを代入していた版は変換されず、Compilerなしと同じ回数になりました。Compilerに頼る場合も、Providerのコンポーネントが実際に変換されたかをReact DevToolsのMemoバッジやビルドログで確かめてください。Compilerと各フックの関係はReactフック(Hooks)とは?全18種の一覧とReact Compiler時代の使い分け【2026年8月版】で扱っています。
Next.js App RouterでContextを使う配置(16.4.3で検証)
‘use client’を付けたProviderコンポーネントでchildrenを包む形
Providerの側でuseStateを持つ、つまり値を画面操作で変えるなら、Providerは必ずClient Componentです。'use client'を付けたファイルにchildrenを受け取るProviderを書き、Server Componentのlayoutから使います。
// app/theme-provider.js
'use client';
import { createContext, useMemo, useState } from 'react';
export const ThemeContext = createContext({ theme: 'light', setTheme: () => {} });
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const value = useMemo(() => ({ theme, setTheme }), [theme]);
return <ThemeContext value={value}>{children}</ThemeContext>;
}
childrenとして渡したServer Componentは、Providerがクライアント側にあってもServer Componentのまま描画されます。Providerは必要な範囲のlayoutに置き、ルートのapp/layout.jsに全部を積み上げないほうが、静的に描画できる範囲を広く保てます。Server ComponentとClient Componentの境界はReact Server Components(RSC)とは?サーバー・クライアントの違いと使い分けを実装例で解説で詳しく扱っています。
Server Componentから<Context value>を直接描画する形(React 19.3以降)
値がサーバー側で決まり、クライアントで変えないなら、ラッパーのコンポーネントは要りません。React #35675(2026-02-03マージ、19.3.0のCHANGELOGに「Allow Server Components to render context providers from client modules」と記載)で、'use client'のモジュールからimportしたContextを、Server ComponentがそのままProviderとして描画できるようになりました。
// app/theme.js
'use client';
import { createContext, use } from 'react';
export const ThemeContext = createContext('light');
export function ThemeLabel() {
return <p>theme={use(ThemeContext)}</p>;
}
layoutはServer Componentのままで、'use client'を付けません。
// app/layout.js(Server Component)
import { ThemeContext } from './theme';
export default function RootLayout({ children }) {
return (
<html lang="ja">
<body>
<ThemeContext value="dark">{children}</ThemeContext>
</body>
</html>
);
}
Next.js 16.4.3(同梱のReactは19.3.0-canary-278794d7-20261002)でこの構成をnext buildすると、ビルドは通り、プリレンダーされたHTMLにtheme=darkが出力されました。Next.jsのServer and Client Componentsのドキュメントは2026-10-26時点でも「React context is not supported in Server Components」とラッパー方式だけを案内しており、実装のほうが先に進んでいます。渡せるのはReactがシリアライズできる値だけで、valueにServer Functionなどの例外に該当しない通常の関数を渡すと「Functions cannot be passed directly to Client Components」でプリレンダーが止まりました。Next.js 16系の変更点はNext.js 16.3とは?Turbopack永続キャッシュとInstant Navigationsなど新機能を実装者目線で解説で整理しています。
Server Componentの中でContextを作る・読むときのエラー
直接描画できるようになったのは「client側で作ったContextに値を渡す」ことだけで、Server ComponentがContextを持つわけではありません。同じNext.js 16.4.3で次の2つを試すと、どちらもビルドで止まりました。
| Server Componentでの操作 | 結果 |
|---|---|
| createContextを呼ぶ | Turbopackのビルドエラー |
| use(ThemeContext)で読む | プリレンダー時のエラー |
createContextをServer Componentのlayoutに書くと「You’re importing a module that depends on createContext into a React Server Component module. This API is only available in Client Components.」と表示され、'use client'を付けるよう促されます。use(ThemeContext)をServer Componentで呼ぶと「Cannot use() an already resolved Client Reference.」で止まります。Contextを読むのは常にClient Componentです。
Contextで押し切らず外部ストアへ切り替える判断基準
状態の性質別に見た置き場所の対応表
| 状態の性質 | 置き場所 |
|---|---|
| テーマ・言語・ログインユーザー | Context |
| 1画面内で完結するUI状態 | useState |
| 頻繁に更新され多数が一部だけ読む値 | Zustand・Jotai |
| 多人数で更新規則を揃えたい値 | Redux Toolkit |
| サーバーから取得したデータ | TanStack Query等 |
Contextが向くのは、更新が少なく、読む側がほぼ値全体を使うものです。Contextには「値の一部だけを購読する」仕組みが無いので、大きなオブジェクトの1プロパティが変わるだけで全読み手が再レンダーします。分割で吸収できないほど種類が増えたら、セレクタで一部だけ購読できるZustandとは?Reactの状態管理を最小コードで実現する使い方を徹底解説【v5対応】やJotaiとは?atomで組むReact状態管理の使い方とv3の変更点【3.0.1対応】へ移る時期です。更新の手順をチームで揃えたい規模ならRedux Toolkitとは?createSliceとRTK Queryの実装と採用判断【2026年8月版】が候補になります。
この判断はReact Nativeでも変わりません。ContextのAPIはWebと共通なので、モバイルアプリでも同じ分割と置き場所の基準がそのまま使えます。React NativeやFlutterでのアプリ開発の相談はFlutter / React Native開発で受け付けています。
Contextに管理させない状態(フォーム入力とAPIデータ)
フォームの入力値をContextに置くと、1文字打つたびにContextを読む全コンポーネントが再レンダーします。入力中の値はフォームのコンポーネント内のuseStateに留め、確定した値だけを上位に渡します。
キャッシュや再取得が必要なAPIデータの管理は、Contextだけで実装せず、データ取得ライブラリやフレームワークに任せます。Context自体には、キャッシュの破棄、再取得、読み込み中やエラーの管理機能がないためです。そのうえで、管理済みのデータやPromiseを子孫へ配る用途ならContextを併用できます。データ管理の役割はTanStack Queryのようなデータ取得ライブラリが持っており、詳しくはTanStack Queryとは?React Queryとの違い・v5の使い方と既定値の落とし穴で扱っています。非同期の更新を画面に反映する仕組みはAsync Reactとは?Transition・Suspense・useOptimisticで非同期UIを組む実装【React 19.3】も参考になります。
よくある質問
useContextとuseはどちらを使うべきですか?
トップレベルで読むだけならどちらでも同じ値が返ります。条件分岐やループの内側で読みたいときだけuseが必要です。useはtry〜catchの中では呼べないので、エラー処理はError Boundaryで行います。
Context.ProviderやContext.Consumerはもう書けなくなりますか?
React 19.3.1ではどちらも動作し、Context.ProviderはContextと同じオブジェクトです。ただしReactは将来のバージョンでContext.Providerを非推奨にすると告知しており、Context.Consumerは非推奨の書き方です。新規コードは<Context>とuseContextで書き、既存コードはcodemodのremove-context-providerで置き換えられます。
createContextの既定値には何を渡すべきですか?
Providerが無くても動いて困らない値(テーマの'light'など)は実際の値を、Provider無しで読まれたら不具合になる値はnullを渡します。nullにした場合は、読み取り用のカスタムフックで検出して例外を投げると置き忘れに気づけます。
Contextを使うとアプリ全体が再レンダーされますか?
Contextの値の変更は、そのContextを読むコンポーネントの再レンダーを引き起こします。その再レンダーから子孫にも通常の描画処理が進むため、propsが変わらない子孫も、memoや同じJSXの再利用などで省略されなければ再レンダーされます。ただしvalueにオブジェクトを直書きすると、親が描画されるたびに読み手全員が再レンダーします。useMemoでの固定、値と更新関数の分割、React Compilerの導入のいずれかで抑えられます。
Server ComponentでuseContextを呼べますか?
呼べません。Contextを読むのはClient Componentだけです。React 19.3以降は、'use client'のファイルで作ったContextにServer Componentから値を渡して描画することはでき、Next.js 16.4.3でも動作しました。