はじめに
React アプリが成長するにつれて、コンポーネント間で共通の値を渡す「props ドリリング」が増え、保守性が下がる課題に直面します。Context API と useContext
フックは、この問題を標準機能だけで解決できる強力な仕組みです。本連載記事では公式ドキュメント https://react.dev/reference/react/useContext をベースに、基礎・設計・最適化・型安全・周辺ライブラリ まで 2 万文字超で徹底的に解説します。第一部では基本概念と最低限動くコード、スコープ設計、props ドリリングの解消フローをじっくり掘り下げます。
- Context API の全体像
- props ドリリング問題とContextの必要性
- Context の作成と Provider 実装
- useContext の基本挙動
- スコープ設計:いつ Provider を分割すべきか
- サンプル:Todo アプリでの Context 分離
- パフォーマンス最適化と再レンダー制御
- useReducer と Context を組み合わせたグローバルストア
- 型安全カスタムフックの作成(TypeScript)
- React 18 コンカレント特有の落とし穴
- Suspense for Data Fetching と Context
- テストと Storybook パターン
- よくあるエラーと解決策
- まとめ
Context API の全体像
React.createContext
で「箱」を作成- Provider が値を注入
- useContext で最寄りの Provider から値を取得
- Provider をネストすれば “局所グローバル” を作れる
図式化すると次のようになります。
<Context.Provider value={A}>
└─ <ChildA /> ← useContext ⇒ A
└─ <Context.Provider value={B}>
└─ <ChildB /> ← useContext ⇒ B
外側と内側で値が上書きされるため、テーマやロケールなど「場面に応じて変わる設定」を自然に切り替えられます。
props ドリリング問題とContextの必要性
propsドリリングとは?
親→子→孫→ひ孫…と同じPropsをリレーして渡す状態。深いツリーでは可読性が悪化し、途中の中間コンポーネントが無関係のPropsを抱える。
Contextで解決
Providerをルート付近に置き、任意の深さで直接useContext
。不要な中継Propsが消え、各コンポーネントが必要な値だけを購読できます。
具体例
function App() {
const user = { id: 1, name: 'Taro' };
return <Page user={user} />;
}
function Page({ user }) { /* ← 中継だけ */
return <Sidebar user={user} />;
}
function Sidebar({ user }) { /* ← 中継だけ */
return <UserProfile user={user} />;
}
function UserProfile({ user }) {
return <p>{user.name}</p>; /* ← 本当に必要なのはここだけ */
}
Context を導入すると、中継 Props をすべて削除し、下記のようにシンプル化できます。
const UserContext = createContext(null);
function App() {
const user = { id: 1, name: 'Taro' };
return (
<UserContext.Provider value={user}>
<Page />
</UserContext.Provider>
);
}
function UserProfile() {
const user = useContext(UserContext);
return <p>{user.name}</p>;
}
Context の作成と Provider 実装
createContext の引数
const ThemeContext = React.createContext('light'); // デフォルト値
1 引数めは フォールバック値。Provider がツリーになかった場合に限り返されるため、開発中の警告用や Storybook のダミー値として活用できます。
Provider に渡す value
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
ポイント
- Provider が再レンダーされると value の参照比較で下位ツリー全部が再レンダー
value
がオブジェクトならuseMemo
で安定化必須
const memoValue = useMemo(() => ({ theme, toggle }), [theme]);
useContext の基本挙動
const value = useContext(ThemeContext);
- 最も近い Provider の value を返す
- Provider が見つからない場合は createContext 時のデフォルト値
- 複数回呼んでも React がキャッシュするためコストは低い
注意:
useContext
は必ず React のレンダーフェーズ内で呼ぶ。コールバックや条件分岐で早期 return する位置に入れるとルール違反。
スコープ設計:いつ Provider を分割すべきか
ケース | 1 Provider | 複数 Provider |
---|---|---|
テーマ、ロケールなど不変に近い設定 | ◯ | △ |
頻繁に変わる値(カーソル座標等) | △ | ◯ |
複数ドメインデータ(Auth と Cart) | △ | ◯ |
複数の値が同時に更新されるわけではない場合、それぞれ独立した Provider に分けることで再レンダー範囲を局所化できます。
サンプル:Todo アプリでの Context 分離
1. アプリ共通設定
const SettingsContext = createContext({
showCompleted: true,
toggleShow: () => {}
});
2. ログインユーザー
const AuthContext = createContext({
user: null,
login: () => {},
logout: () => {}
});
3. Todo リスト
todo はサイズが大きく頻繁に変わるため、Context より useReducer + Context の複合パターンが適切です(第二部で詳細解説)。
パフォーマンス最適化と再レンダー制御
Context API は便利ですが、Provider の value
が変わるたびに配下すべての useContext
呼び出しが再レンダー されます。大規模ツリーでは深刻なパフォーマンス低下につながるため、次の 4 つの対策を組み合わせましょう。
1. useMemo で value オブジェクトを安定化
const SettingsProvider = ({ children }) => {
const [showCompleted, setShowCompleted] = useState(true);
const ctx = useMemo(
() => ({ showCompleted, toggle: () => setShowCompleted(v => !v) }),
[showCompleted] // プリミティブのみ依存
);
return <SettingsContext.Provider value={ctx}>{children}</SettingsContext.Provider>;
};
オブジェクト内の参照が変わらなければ再レンダーは下位に波及しません。
2. Context を分割する
更新頻度 が異なる値を 1 つの Provider に詰め込むと、滅多に変わらない設定が毎回レンダーのトリガーになります。
AuthContext
とThemeContext
を個別 Provider にする- パフォーマンスクリティカルな配列や Map は専用 Context へ切り出す
3. Context Selector パターン
ライブラリ use-context-selector
を使うと、特定プロパティだけを購読 でき、不要な再レンダーを劇的に減らせます。
import { createContext, useContextSelector } from 'use-context-selector';
const CountContext = createContext({ count: 0, inc: () => {} });
function CounterLabel() {
const count = useContextSelector(CountContext, v => v.count);
return <span>{count}</span>;
}
Provider の値がオブジェクト再生成されても count
が変わらない限り CounterLabel
は再レンダーされません。
4. memo + useContext は要注意
React.memo
でラップしても、Context 更新は強制的に渡ってくるため 再レンダー抑止はできません。必要に応じ「コンテナ–プレゼンテーション分割」を行い、Context を読む部分をコンテナ、表示だけ行う部分を memo
化すると効果的です。
useReducer と Context を組み合わせたグローバルストア
小規模アプリなら Redux などを導入せずに、useReducer
+ Context で十分なグローバルストアを構築できます。
const TodoContext = createContext(null);
function todoReducer(state, action) {
switch (action.type) {
case 'add':
return [...state, action.payload];
case 'toggle':
return state.map(t =>
t.id === action.id ? { ...t, done: !t.done } : t
);
default:
return state;
}
}
export function TodoProvider({ children }) {
const [todos, dispatch] = useReducer(todoReducer, []);
const ctx = useMemo(() => ({ todos, dispatch }), [todos]);
return <TodoContext.Provider value={ctx}>{children}</TodoContext.Provider>;
}
呼び出し側は dispatch({ type: 'add', payload })
で状態を更新し、todos
を購読できます。Redux に比べてボイラープレートが少ないのが利点ですが、ミドルウェア機構 や 開発ツール が欲しくなった時点で Redux Toolkit へ移行する判断基準となります。
型安全カスタムフックの作成(TypeScript)
export const SettingsContext =
createContext<SettingsCtx | undefined>(undefined);
export function useSettings(): SettingsCtx {
const ctx = useContext(SettingsContext);
if (!ctx) {
throw new Error('useSettings must be inside <SettingsProvider>');
}
return ctx;
}
undefined
をコンテキスト型に含め、Provider 外使用をコンパイル時に検出- 呼び出し側は
const { theme } = useSettings();
とシンプル
React 18 コンカレント特有の落とし穴
並列レンダーでは 「旧ツリー」と「新ツリー」が同時に存在 します。Context の値もレンダー単位でスナップショットされるため、更新が重なった場合は “数フレームだけ古いテーマで描画” のような一貫性揺らぎが起こり得ます。
対策:
- Transition 内で Provider を更新し、旧ツリーが素早く置き換わるようにする
- 非同期値(サーバフェッチ結果など) は
useSyncExternalStore
で同期を取る
Suspense for Data Fetching と Context
React 18 の サスペンス対応データフェッチライブラリ(TanStack Query, SWR v2 等)は、Context でキャッシュを共有する設計が主流です。
<QueryClientProvider client={client}> // Provider
<App />
</QueryClientProvider>
内部的に Provider の値が変わらないよう慎重に実装されているため、大量のデータを扱っても再レンダーが抑えられます。
テストと Storybook パターン
単体テスト
- Provider をラップしたテストユーティリティを作成
renderHook
でカスタムフックのみをテスト
const wrapper = ({ children }) => <AuthProvider>{children}</AuthProvider>;
const { result } = renderHook(() => useAuth(), { wrapper });
Storybook
decorators
で Provider を追加し、Context に基づく UI を再現- 複数バリエーションを Template で切り替え、locale や theme を擬似表示
よくあるエラーと解決策
エラー | 原因 | 対処 |
---|---|---|
Cannot read property 'xyz' of null | Provider がツリーに含まれていない | カスタムフックでundefinedチェック |
遅延評価したい値が毎回変わる | Provider の value を毎レンダー生成 | useMemo で固定 or 値を分割Providerへ |
子が不必要に再レンダー | value がオブジェクトリテラル | useMemo + useCallback で参照安定化 |
まとめ
Context の基礎とスコープ設計、パフォーマンス最適化と useReducer 連携・型安全化・並列レンダーの留意点まで解説しました。ここまでを理解すれば、中小規模の React アプリなら Redux 等を導入せずとも十分スケーラブルな状態共有が可能です。
コメント