React: コンテキストとグローバル状態

最終更新:2026-10-26

1. 学習内容



2. テーマの変更に関する話

(1) 課題:5階層のコンポーネントを通過するプロップ

アリスは、アプリに「ダークモード」の切り替え機能を追加したいと考えています。データフローは、5層のコンポーネントを経由する必要があります:

▶ サンプル:コンテキストの使用

出力:

TEXT
Context ベースの状態をコンポーネント間で共有
▶ 試してみよう

出力:

TEXT
App コンポーネントの状態は 'light'。インタラクティブな UI をレンダリング。

アリスの質問:

(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

100%
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を使用してください。

📖 まとめ


📝 練習問題

  1. 基本問題(難易度 ⭐):中国語と英語の切り替えを可能にする locale (zh/en) および toggleLocale() メソッドを提供する LocaleProvider + useLocale フックを作成してください。
  2. 上級演習(難易度 ⭐⭐):ユーザーの認証ステータスを管理するための user、login(username, password)、および logout() メソッドを提供する AuthProvider + useAuth フックを作成してください。
  3. 課題(難易度:⭐⭐⭐):CartProvider と useCart を組み合わせたフックを作成し、Context と useReducer を使用して、商品の追加、削除、数量の変更、カートのクリアを行う機能を備えたショッピングカートを実装してください。useMemo を使用してコードを最適化し、不要な再レンダリングを回避してください。
Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%