React: コンテキストとグローバル状態
最終更新:2026-10-26
1. 学習内容
- createContext と Provider
- useContext: 消費コンテキスト
- コンテキストとプロップスの掘り下げ
- 多階層ネストとパフォーマンスの最適化
- コンテキストとuseReducerの組み合わせ(軽量な状態管理)
2. テーマの変更に関する話
(1) 課題:5階層のコンポーネントを通過するプロップ
アリスは、アプリに「ダークモード」の切り替え機能を追加したいと考えています。データフローは、5層のコンポーネントを経由する必要があります:
▶ サンプル:コンテキストの使用
出力:
TEXT
Context ベースの状態をコンポーネント間で共有
出力:
TEXT
App コンポーネントの状態は 'light'。インタラクティブな UI をレンダリング。
アリスの質問:
themeデータがアプリから各レイヤーを経由してボタンへと渡されていく過程で、その間の3つのレイヤーは実際にはこのデータを必要としない- 渡すユーザー情報をもう1つ追加すると、propsが極端に膨れ上がってしまいます。
- これは定番のプロップス・ドリリングです
(2) 「コンテキスト」の解答
▶ 試してみようメリット:中間コンポーネント(Page、MainLayout、Sidebar)は、theme プロパティを渡す必要がなくなりました。データはプロバイダーからコンシューマーへ直接渡されます。
3. コンテキストの基本概念
| API | 関数 | 引数/戻り値 |
|---|---|---|
createContext(defaultValue) |
コンテキストコンテナを作成する | { Provider, Consumer } に戻る |
<Context.Provider value={...}> |
サブツリーにデータを提供します | value: 任意の型。変更があるとコンシューマーの再レンダリングがトリガーされます |
useContext(Context) |
最新のプロバイダーデータを取得する | プロバイダーの value を返す。プロバイダーが見つからない場合は、デフォルト値を返す |
カスタム useXxx() フック |
useContext のラップ + エラーチェック | 推奨パターン:Provider が存在しない場合に明確なエラーをスローする |
(1) createContext + Provider + useContext
graph TB
A[createContext<br/>Create a context container] --> B[Provider<br/>Wrapper Component tree<br/>Provide value]
B --> C[Middle Component<br/>Don't care Context]
C --> D[useContext<br/>Consumption Context Data]
style A fill:#1890ff,color:#fff
style B fill:#52c41a,color:#fff
style D fill:#ff6b6b,color:#fff
(2) コンテキストが適用されるシナリオ
| 文脈に適している | 文脈に適していない |
|---|---|
| テーマ(ダークモード) | コンポーネントの設定パラメータ(Props を使用) |
| ユーザーログイン情報 | フォームの入力値(ローカル状態を使用) |
| 言語・国際化 | APIデータ(TanStack Queryを使用) |
| ルート状態 | アニメーション状態(アニメーションライブラリによって管理される) |
| グローバル設定 | シンプルな親から子へのプロパティ渡し(プロパティの直接使用) |
ルール:「複数のレベルや複数のコンポーネント」間でデータを共有する必要がある場合にのみ、Context を使用してください。単に親コンポーネントと子コンポーネントの間で値を渡し合うだけなら、Props で十分です。
4. コンテキストのネストとプロバイダーのコンポーネント化
(1) マルチレベルなコンテキストのネスト
実際のプロジェクトには、通常、複数のコンテキストが存在します:
JSX
function App() {
return (
<AuthProvider>
<ThemeProvider>
<I18nProvider>
<NotificationProvider>
<MainApp />
</NotificationProvider>
</I18nProvider>
</ThemeProvider>
</AuthProvider>
)
}
ネスト地獄を避けるために、AppProviders 複合コンポーネントを作成することができます:
JSX
function AppProviders({ children }) {
return (
<AuthProvider>
<ThemeProvider>
<I18nProvider>
<NotificationProvider>
{children}
</NotificationProvider>
</I18nProvider>
</ThemeProvider>
</AuthProvider>
)
}
function App() {
return (
<AppProviders>
<MainApp />
</AppProviders>
)
}
(2) カスタムプロバイダーフック
▶ 試してみよう5. コンテキストのパフォーマンス最適化
コンテキストの値が変更されると、そのコンテキストを利用しているすべてのコンポーネントが再レンダリングされます。これが、コンテキストにおける最大のパフォーマンス上の落とし穴です。
| 最適化手法 | 原理 | 適用可能なシナリオ |
|---|---|---|
useMemo 安定した値 |
レンダリングのたびに新しいオブジェクト参照を作成しない | プロバイダーの値に関数やオブジェクトが含まれている場合 |
| コンテキストごとの分割 | 「変化する」データと「変化しない」データは、それぞれ異なるコンテキストに割り当てられる | 安定したデータのみを必要とするコンシューマーもある |
React.memo コンシューマーのラップ |
プロパティに変更がない場合の再レンダリングのスキップ | コンシューマーコンポーネントが他のプロパティを受け取った場合 |
| 選択的サブスクリプション | セレクタを使用して、必要なスライスのみを取得 | Zustand/Redux による精密な更新の実装 |
JSX
// ❌ Question:Every time App Rendering,value They're all new partners. → All Consumer All are heavily rendered
function App() {
const [count, setCount] = useState(0)
return (
<ThemeContext.Provider value={{ theme: 'dark', setCount }}>
<ExpensiveTree />
</ThemeContext.Provider>
)
}
// ✅ Fix 1:use useMemo Stable value Quote
function App() {
const [count, setCount] = useState(0)
const themeValue = useMemo(
() => ({ theme: 'dark', toggleTheme: () => setCount(c => c + 1) }),
[]
)
return (
<ThemeContext.Provider value={themeValue}>
<ExpensiveTree />
</ThemeContext.Provider>
)
}
// ✅ Fix 2:separate "unchanged" and "changed" data into different Context
const SettingsContext = createContext({ theme: 'light', language: 'zh' })
const CountContext = createContext(0)
function App() {
const [count, setCount] = useState(0)
return (
<SettingsContext.Provider value={settingsValue}> {/* useMemo Stable */}
<CountContext.Provider value={count}>
<ExpensiveTree />
</CountContext.Provider>
</SettingsContext.Provider>
)
}
// Only when used CountContext The component will be count Re-render on changes
// SettingsContext Consumers will not be affected
6. コンテキストと useReducer:軽量なグローバル状態の構築
Contextの「レベル間伝播」とuseReducerの「複雑な状態管理」を組み合わせることで、軽量なグローバル状態ソリューションを構築できます:
▶ 実行例を展開(125 行)
出力:
TEXT
表示:"Button"。状態:theme(setter:setTheme)。ボタン:Button
期待される出力:完全な通知システム。上部には(未読通知数を表示する)通知ベルが表示され、中央ではユーザーが「情報」「成功」「エラー」の3種類の通知を送信でき、下部には通知の一覧が表示され、各通知を「既読」にマークしたり削除したりすることができます。
▶ サンプル:ユーザー認証ステータスの管理(コンテキスト + カスタムフック)
▶ 実行例を展開(114 行)
出力:
TEXT
メールアドレス/パスワードのログインフォーム。メールアドレスの形式が不正な場合 → 赤い枠線 + "The email address format is incorrect."。パスワードが 6 文字未満 → "Password must be at least 6 chars"。送信 → console.log({email, password})
出力:
TEXT
サブタイトル:"🔐 Log In"。表示:"🔐 Log In"。状態:user(setter:setUser)、loading(setter:setLoading)、username(setter:setUsername)。ボタン:{loading ? 'Logging in......' : 'Log In'}、Log Out。入力:Username(Input admin Experience the Role of an Administrator)、Password。送信処理付きのフォーム。useMemo で計算を最適化。useCallback でハンドラ関数の参照をキャッシュ。Context によるグローバル状態。非同期データ取得/読み込み状態。タイマーベースの挙動
▶ サンプル:i18n 国際化による複数のプロバイダ層の統合
▶ 試してみよう出力:
TEXT
国際化:言語スイッチャーがすべてのテキストを変更する。Context + provider パターンで多言語に対応
出力:
TEXT
サブタイトル:"{t('greeting')}"。表示:"Chinese"。状態:locale(setter:setLocale)。ドロップダウン:Chinese、English。useMemo で計算を最適化。useCallback でハンドラ関数の参照をキャッシュ。Context によるグローバル状態
▶ サンプル:パフォーマンスを最適化するためのコンテキストの分割
▶ 試してみよう出力:
TEXT
SplitContextApp コンポーネントの状態は:null、false。インタラクティブな UI をレンダリング。
出力:
TEXT
表示:"Login"。状態:user(setter:setUser)、loading(setter:setLoading)。ボタン:login('Alice')}>Login。useMemo で計算を最適化。Context によるグローバル状態。非同期データ取得/読み込み状態。タイマーベースの挙動
▶ サンプル:プロップドリリングの代替として useContext を使用する完全な例
▶ 試してみよう
出力:
TEXT
SettingsApp コンポーネントの状態は:{
theme: 'light', fontSize: 14, sidebarCollapsed: false,
}。インタラクティブな UI をレンダリング。
❓ よくある質問
Q
Contextのvalueが毎回新しいオブジェクトになり、その結果すべてのConsumerが再レンダリングされてしまう場合はどうすればよいですか?A
useMemoを使用して、valueへの参照を固定してください:const value = useMemo(() => ({ theme, toggleTheme }), [theme])。また、「安定した」データと「変化する」データを別々の Context に分けることもできます。最適化後もパフォーマンスの問題が解消されない場合は、Zustand の使用を検討してください(Zustand は精密な更新をサポートしており、無関係なコンポーネントの再レンダリングを引き起こしません)。Q コンテキストのネスト階層が多すぎる場合はどうすればよいですか?
A ① プロバイダー(前述の
AppProviders など)を使用してコンポーネントをグループ化し、ネスト構造をカプセル化します。② 本当にそれほど多くのコンテキストが必要かどうかを検討してください。一部の「グローバル」データは、実際には単に「ローカルなグローバル」データ(特定のページの設定など)に過ぎず、アプリの最上位コンテキストに配置する必要はありません。Q カスタム
useXxx フック内で、context が null かどうかを確認するのはなぜですか?A これは防御的プログラミングの手法です。開発者が
useContext を使用する前にコンポーネントを Provider でラップするのを忘れた場合、context は null になるか(createContext にデフォルト値が渡されていない場合)、デバッグが困難なエラーを引き起こします。早い段階で明確なエラーメッセージをスローする方が、ユーザーがブラウザのコンソールで「Cannot read property of null」というメッセージを目にするよりも、はるかにユーザーフレンドリーです。Q ContextはReduxの代わりになりますか?
A 中小規模のプロジェクトであれば可能ですが、大規模なプロジェクトでは不可能です。Contextには、Reduxが持つ以下の3つの機能が欠けています:① ミドルウェアの仕組み(ロギング、非同期処理、永続化);② タイムトラベルデバッグ(DevToolsでの状態変化の再生); ③ 選択的サブスクリプション(Contextの値が変更されるとすべてのコンシューマーが再レンダリングされますが、Reduxではセレクターを使用して必要な部分のみをサブスクライブできます)。プロジェクトでこれら3つの機能が必要な場合は、Zustand(軽量)またはRedux Toolkitを使用してください。
📖 まとめ
createContext→.Provider→useContextContextを使用するための3つの手順- ContextはProps Drillingの問題を解決しますが、「状態管理」の問題は解決しません
- パフォーマンス上の落とし穴:コンテキスト値の変更により、すべてのコンシューマーが再レンダリングされます。
useMemoを使用して最適化してください。 useXxxフックのカスタマイズとエラーチェックは、推奨されるベストプラクティスですContext + useReducerは軽量なグローバル状態管理ソリューションとして機能しますが、複雑なシナリオでは、やはり Zustand や Redux を使用するのが最善です。
📝 練習問題
- 基本問題(難易度 ⭐):中国語と英語の切り替えを可能にする
locale(zh/en) およびtoggleLocale()メソッドを提供するLocaleProvider+useLocaleフックを作成してください。 - 上級演習(難易度 ⭐⭐):ユーザーの認証ステータスを管理するための
user、login(username, password)、およびlogout()メソッドを提供するAuthProvider+useAuthフックを作成してください。 - 課題(難易度:⭐⭐⭐):
CartProviderとuseCartを組み合わせたフックを作成し、Context と useReducer を使用して、商品の追加、削除、数量の変更、カートのクリアを行う機能を備えたショッピングカートを実装してください。useMemoを使用してコードを最適化し、不要な再レンダリングを回避してください。