React を使っていると、「このテーマ設定を全コンポーネントに渡すには props をバケツリレーするしかない……」と途方に暮れたことはありませんか?そんなとき救世主となるのが Context と useContext フックです。本記事では、useContext の基礎から facebook/react リポジトリの実際のソースコードを参照した内部構造の解説、そして実践的なユースケースまでを徹底的に掘り下げます。
1. なぜ useContext が必要か
1.1 Props drilling の問題
React のコンポーネントは通常、親から子へ props を介してデータを渡します。しかし、深くネストしたコンポーネントにデータを渡したい場合、途中のすべてのコンポーネントを経由しなければなりません。これを「Props drilling(Props バケツリレー)」と呼びます。
// ❌ Props バケツリレーの例
function App() {
const [theme, setTheme] = useState('dark');
return <Header theme={theme} />;
}
function Header({ theme }) {
return <Navigation theme={theme} />;
}
function Navigation({ theme }) {
return <Button theme={theme} />; // やっと使う!
}
function Button({ theme }) {
return <button className={`button-${theme}`}>Click</button>;
}
この「Props drilling(Props バケツリレー)」は以下の問題を引き起こします:
-
中間コンポーネントが不要な props を受け取る:
HeaderやNavigationはthemeを使わないのに渡している - リファクタリングが困難: 途中に新しいコンポーネントを追加するたびに props の受け渡しを追加する必要がある
- コードの見通しが悪化: どこでどの props が使われているか追跡しづらい
1.2 Context による解決
Context を使うと、コンポーネントツリーを通じてデータを直接渡すことができます。中間のコンポーネントを経由する必要がありません。
// ✅ Context を使った例
import { createContext, useContext, useState } from 'react';
const ThemeContext = createContext('light');
function App() {
const [theme, setTheme] = useState('dark');
return (
<ThemeContext value={theme}>
<Header />
</ThemeContext>
);
}
function Header() {
return <Navigation />; // theme を渡す必要なし!
}
function Navigation() {
return <Button />; // theme を渡す必要なし!
}
function Button() {
const theme = useContext(ThemeContext); // 直接取得!
return <button className={`button-${theme}`}>Click</button>;
}
1.3 useContext の役割
useContext は次のことを行います:
- コンテクストの値を読み取る: ツリー内で最も近い Provider の値を取得
- 変更をサブスクライブ(購読)する: Provider の値が変更されたら自動的に再レンダー
const value = useContext(SomeContext);
💡 ポイント:
useContextは「ツリー内で最も近い Provider」を探します。同じコンポーネント内にある Provider は考慮されません(上方向のみ探索)。
2. useContext の内部構造を徹底解剖 — facebook/react ソースリーディング
useContext を使うたびに、React の内部では複数のモジュールが連携して動いています。この章では、facebook/react リポジトリの実際のコードを追いながら、Context がどのように動作するかを解説します。
2.0 全体像: Context が動く仕組みの4つのステージ
まず、Context の処理フローを大まかに把握しましょう:
🎨 createContext(コンテクストの作成)
↓
📦 Provider(値を提供)
↓
🔍 useContext(値を読み取り)
↓
🔄 変更検知と再レンダー
それぞれのステージで何が起きているのか、実際のコードと共に見ていきましょう。
2.1 Context の作成: createContext
まず、createContext で何が作られるのか?
// packages/react/src/ReactContext.js
export function createContext<T>(defaultValue: T): ReactContext<T> {
const context: ReactContext<T> = {
$$typeof: REACT_CONTEXT_TYPE,
// 複数のレンダラー(React Native と Fabric など)をサポートするため
// プライマリとセカンダリの2つの値を保持
_currentValue: defaultValue,
_currentValue2: defaultValue,
// 並行レンダリング対応のスレッドカウント
_threadCount: 0,
Provider: (null: any),
Consumer: (null: any),
};
// Provider は context 自身を指す
context.Provider = context;
// Consumer は context への参照を持つ
context.Consumer = {
$$typeof: REACT_CONSUMER_TYPE,
_context: context,
};
return context;
}
Context オブジェクトの構造
| プロパティ | 役割 |
|---|---|
$$typeof |
React 要素の種類を識別するシンボル |
_currentValue |
プライマリレンダラー用の現在値 |
_currentValue2 |
セカンダリレンダラー用の現在値 |
_threadCount |
並行レンダリングのスレッド管理 |
Provider |
値を提供するコンポーネント(context 自身) |
Consumer |
Render Props パターンで値を消費するコンポーネント |
💡ポイント
createContext は「どの情報を共有するか」を定義する識別子のようなものです。実際のデータはここには保持されません。
2.2 エントリポイント: packages/react/src/ReactHooks.js
あなたが useContext を呼ぶとどこに飛ぶのか?
// packages/react/src/ReactHooks.js
export function useContext<T>(Context: ReactContext<T>): T {
const dispatcher = resolveDispatcher();
if (__DEV__) {
// Context.Consumer を useContext に渡すのは間違い!
if (Context.$$typeof === REACT_CONSUMER_TYPE) {
console.error(
'Calling useContext(Context.Consumer) is not supported and will cause bugs. ' +
'Did you mean to call useContext(Context) instead?',
);
}
}
return dispatcher.useContext(Context);
}
何が起きているか?
-
resolveDispatcher()で現在の Dispatcher を取得- Dispatcher は Hooks の実装を切り替える「交通整理役」
-
ReactSharedInternals.Hから取得
-
DEV モードでの検証
-
useContext(Context.Consumer)という間違った使い方を検出
-
-
Dispatcher の
useContextを呼び出し- 実際の処理に委譲
2.3 Dispatcher の実装: マウント時も更新時も同じ
useState と違い、useContext はマウント時も更新時も同じ実装です!
// packages/react-reconciler/src/ReactFiberHooks.js
const HooksDispatcherOnMount: Dispatcher = {
readContext,
useContext: readContext, // ← マウント時
// ... 他の Hooks
};
const HooksDispatcherOnUpdate: Dispatcher = {
readContext,
useContext: readContext, // ← 更新時も同じ!
// ... 他の Hooks
};
// DEV モードでの実装
HooksDispatcherOnMountInDEV = {
useContext<T>(context: ReactContext<T>): T {
currentHookNameInDev = 'useContext';
mountHookTypesDev();
return readContext(context); // 結局 readContext を呼ぶ
},
// ...
};
なぜ同じなのか?
-
useStateなどは「Hook ノード」を作成し、順序で管理する必要がある -
useContextは Hook ノードを作らず、単純に現在の Context 値を読み取るだけ - つまり、
useContextは厳密には他の Hooks とは異なる動作をしています
💡 ポイント:
useContextは他の Hooks と違い、Hook リストにノードを追加しません。そのため、条件分岐の中で呼んでも技術的には動作しますが、一貫性のためにトップレベルで呼ぶことが推奨されます。
2.4 コア実装: readContext
Context の値を実際に読み取る処理
// packages/react-reconciler/src/ReactFiberNewContext.js
let currentlyRenderingFiber: Fiber | null = null;
let lastContextDependency: ContextDependency<mixed> | null = null;
export function readContext<T>(context: ReactContext<T>): T {
if (__DEV__) {
// useMemo や useReducer の中で呼ばれていないかチェック
if (isDisallowedContextReadInDEV) {
console.error(
'Context can only be read while React is rendering. ' +
'In classes, you can read it in the render method or getDerivedStateFromProps. ' +
'In function components, you can read it directly in the function body, but not ' +
'inside Hooks like useReducer() or useMemo().',
);
}
}
return readContextForConsumer(currentlyRenderingFiber, context);
}
readContextForConsumer の詳細
// packages/react-reconciler/src/ReactFiberNewContext.js
function readContextForConsumer<T>(
consumer: Fiber | null,
context: ReactContext<T>,
): T {
// ステップ1: 現在の Context 値を取得
const value = isPrimaryRenderer
? context._currentValue
: context._currentValue2;
// ステップ2: 依存関係アイテムを作成
const contextItem = {
context: ((context: any): ReactContext<mixed>),
memoizedValue: value, // 現在の値を記録
next: null, // 次の依存関係へのポインタ
};
// ステップ3: Fiber に依存関係を登録
if (lastContextDependency === null) {
// このコンポーネントの最初の Context 依存関係
if (consumer === null) {
throw new Error(
'Context can only be read while React is rendering. ...',
);
}
lastContextDependency = contextItem;
consumer.dependencies = {
lanes: NoLanes,
firstContext: contextItem,
};
consumer.flags |= NeedsPropagation;
} else {
// 2つ目以降の依存関係を追加
lastContextDependency = lastContextDependency.next = contextItem;
}
return value;
}
依存関係のリスト構造
Fiber (コンポーネント)
└─ dependencies
└─ firstContext → ThemeContext (value: 'dark')
└─ next → UserContext (value: { name: 'John' })
└─ next → null
💡 ポイント:
readContextは「今の値を返す」だけでなく、「このコンポーネントがどの Context に依存しているか」を記録しています。これが後の変更検知に使われます。
2.5 Provider の仕組み: 値のプッシュとポップ
Provider が値を設定する処理
// packages/react-reconciler/src/ReactFiberNewContext.js
// Context 値を管理するスタック
const valueCursor: StackCursor<mixed> = createCursor(null);
export function pushProvider<T>(
providerFiber: Fiber,
context: ReactContext<T>,
nextValue: T,
): void {
if (isPrimaryRenderer) {
// 現在の値をスタックに保存
push(valueCursor, context._currentValue, providerFiber);
// Context の現在値を更新
context._currentValue = nextValue;
} else {
push(valueCursor, context._currentValue2, providerFiber);
context._currentValue2 = nextValue;
}
}
export function popProvider(
context: ReactContext<any>,
providerFiber: Fiber,
): void {
const currentValue = valueCursor.current;
if (isPrimaryRenderer) {
// スタックから前の値を復元
context._currentValue = currentValue;
} else {
context._currentValue2 = currentValue;
}
pop(valueCursor, providerFiber);
}
スタック構造のイメージ
<ThemeContext value="dark"> // push('dark')
<ThemeContext value="light"> // push('light')
<Button /> // useContext → 'light'
</ThemeContext> // pop() → 'light' を戻す
<Footer /> // useContext → 'dark'
</ThemeContext> // pop() → 'dark' を戻す
2.6 Provider の更新: updateContextProvider
Provider の value が変更された時の処理
// packages/react-reconciler/src/ReactFiberBeginWork.js
let hasWarnedAboutUsingNoValuePropOnContextProvider = false;
function updateContextProvider(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
) {
const context: ReactContext<any> = workInProgress.type;
const newProps = workInProgress.pendingProps;
const newValue = newProps.value;
if (__DEV__) {
// value prop が無いことを警告
if (!('value' in newProps)) {
if (!hasWarnedAboutUsingNoValuePropOnContextProvider) {
hasWarnedAboutUsingNoValuePropOnContextProvider = true;
console.error(
'The `value` prop is required for the `<Context.Provider>`. ' +
'Did you misspell it or forget to pass it?',
);
}
}
}
// 新しい値をスタックにプッシュ
pushProvider(workInProgress, context, newValue);
// 子コンポーネントを処理
const newChildren = newProps.children;
reconcileChildren(current, workInProgress, newChildren, renderLanes);
return workInProgress.child;
}
2.7 変更の伝播: propagateContextChange
Provider の値が変わった時、どうやって子コンポーネントに伝えるのか?
// packages/react-reconciler/src/ReactFiberNewContext.js
export function checkIfContextChanged(
currentDependencies: Dependencies,
): boolean {
// 依存している Context の値が変わったかチェック
let dependency = currentDependencies.firstContext;
while (dependency !== null) {
const context = dependency.context;
const newValue = isPrimaryRenderer
? context._currentValue
: context._currentValue2;
const oldValue = dependency.memoizedValue;
// Object.is で比較
if (!is(newValue, oldValue)) {
return true; // 変更あり!
}
dependency = dependency.next;
}
return false; // 変更なし
}
変更伝播のアルゴリズム
// packages/react-reconciler/src/ReactFiberNewContext.js
function propagateContextChanges<T>(
workInProgress: Fiber,
contexts: Array<any>,
renderLanes: Lanes,
forcePropagateEntireTree: boolean,
): void {
let fiber = workInProgress.child;
while (fiber !== null) {
// 各 Fiber の依存関係をチェック
const list = fiber.dependencies;
if (list !== null) {
let dep = list.firstContext;
while (dep !== null) {
for (let i = 0; i < contexts.length; i++) {
const context = contexts[i];
if (dep.context === context) {
// 一致!このコンポーネントの再レンダーをスケジュール
consumer.lanes = mergeLanes(consumer.lanes, renderLanes);
scheduleContextWorkOnParentPath(
consumer.return,
renderLanes,
workInProgress,
);
}
}
dep = dep.next;
}
}
// 子孫を走査
fiber = fiber.child ?? fiber.sibling;
}
}
💡ポイント
React は Fiber ツリーを走査し、変更された Context に依存するすべてのコンポーネントを見つけて再レンダーをスケジュールします。Object.is で比較するため、オブジェクトの参照が変わると再レンダーが発生します。
2.8 全体の流れを図解
フロー1: Context の作成と提供
フロー2: useContext による値の読み取り
フロー3: Provider の値変更と再レンダー
2.9 まとめ: useContext の内部構造
useContext が動く仕組みの4ステージ(再掲)
-
createContext: Context オブジェクトを作成(
_currentValueを初期化) -
Provider:
pushProviderで値をスタックに設定 - useContext / readContext: 現在の値を取得し、依存関係を登録
-
変更検知:
propagateContextChangeで依存コンポーネントを再レンダー
重要な設計原則
| 原則 | 理由 |
|---|---|
| Provider は上位に配置 | useContext は上方向のみ探索するため |
| value の参照を安定させる | Object.is で比較されるため、毎回新しいオブジェクトを渡すと再レンダーが発生 |
| デフォルト値は Provider がない時のみ使用 |
value={undefined} でも Provider があればデフォルト値は使われない |
3. 代表的ユースケース
3.1 テーマの共有
const ThemeContext = createContext<'light' | 'dark'>('light');
function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
return (
<ThemeContext value={{ theme, setTheme }}>
{children}
</ThemeContext>
);
}
function ThemeToggle() {
const { theme, setTheme } = useContext(ThemeContext);
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Current: {theme}
</button>
);
}
3.2 認証状態の管理
const AuthContext = createContext<{
user: User | null;
login: (credentials: Credentials) => Promise<void>;
logout: () => void;
} | null>(null);
function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
}
3.3 Context + useReducer でスケールアップ
const TodoContext = createContext<{
todos: Todo[];
dispatch: Dispatch<TodoAction>;
} | null>(null);
function TodoProvider({ children }) {
const [todos, dispatch] = useReducer(todoReducer, []);
return (
<TodoContext value={{ todos, dispatch }}>
{children}
</TodoContext>
);
}
4. パフォーマンス最適化
4.1 オブジェクトや関数を渡す時の再レンダー問題
// ❌ 毎回新しいオブジェクトが作られ、すべての消費者が再レンダー
function MyApp() {
const [user, setUser] = useState(null);
return (
<AuthContext value={{ user, login: () => {} }}>
<Page />
</AuthContext>
);
}
// ✅ useMemo と useCallback で参照を安定させる
function MyApp() {
const [user, setUser] = useState(null);
const login = useCallback(async (credentials) => {
const user = await api.login(credentials);
setUser(user);
}, []);
const value = useMemo(() => ({ user, login }), [user, login]);
return (
<AuthContext value={value}>
<Page />
</AuthContext>
);
}
4.2 Context の分割
// ❌ 1つの大きな Context
const AppContext = createContext({ theme, user, settings, ... });
// ✅ 関心ごとに分割
const ThemeContext = createContext(theme);
const UserContext = createContext(user);
const SettingsContext = createContext(settings);
5. トラブルシューティング
5.1 Provider に渡した値がコンポーネントから見えない
よくある原因:
-
useContext()を呼ぶコンポーネントと同じ(または下位の)場所で Provider をレンダーしている - Provider でラップし忘れている
- ビルドツールの問題で Context が重複している
// ❌ 同じコンポーネント内の Provider は見えない
function MyComponent() {
return (
<ThemeContext value="dark">
<Child />
</ThemeContext>
);
// この位置で useContext(ThemeContext) しても "dark" は見えない
}
5.2 デフォルト値ではなく undefined が返ってくる
// ❌ value prop を忘れている
<ThemeContext>
<Button />
</ThemeContext>
// ❌ props 名を間違えている
<ThemeContext theme={theme}>
<Button />
</ThemeContext>
// ✅ 正しい使い方
<ThemeContext value={theme}>
<Button />
</ThemeContext>
6. まとめ
この記事で解説した内容は、facebook/react リポジトリの以下のファイルと公式ドキュメントに基づいて作成しました
Context の作成
-
packages/react/src/ReactContext.js-
createContext関数
-
useContext のエントリポイント
-
packages/react/src/ReactHooks.js-
useContextのエクスポート関数
-
コア実装
-
packages/react-reconciler/src/ReactFiberNewContext.js-
readContext: Context 値の読み取り -
readContextForConsumer: 依存関係の登録 -
pushProvider/popProvider: Provider のスタック管理 -
propagateContextChange: 変更の伝播 -
checkIfContextChanged: 変更検知
-
Provider の更新処理
-
packages/react-reconciler/src/ReactFiberBeginWork.js-
updateContextProvider: Provider の更新処理 -
updateContextConsumer: Consumer の更新処理
-
Dispatcher の定義
-
packages/react-reconciler/src/ReactFiberHooks.js-
HooksDispatcherOnMount/HooksDispatcherOnUpdate
-
-
useContextは「Props バケツリレー」を解消し、コンポーネントツリー全体でデータを共有できる - 内部では Context オブジェクトの
_currentValueを直接読み取り、依存関係を Fiber に登録している - Provider の値が変わると、React は依存コンポーネントを走査して再レンダーをスケジュールする
- パフォーマンスのために、
useMemo/useCallbackで参照を安定させ、Context を適切に分割することが重要
状態管理の分岐点: グローバルに共有すべきデータ(テーマ、認証、ロケール)には Context が適切。頻繁に変更されるデータや複雑な状態には、外部ストア(Zustand, Jotai など)の併用を検討しましょう。