関心のないコンポーネントの層を介して props を渡していく作業は、非常に退屈なものです。ある日は新機能をリリースしているのに、翌日にはたった一つの prop の名前を変更するためだけに 6 つものファイルを編集している、といったことが起こります。これが Prop Drilling(プロップ・ドリリング)の正体です。親がデータを持っており、深い階層にある子がそれを必要としているとき、その間にあるすべてのコンポーネントが「運び屋」になってしまいます。アプリは動作し続けますが、コードベースは脆くなります。中間層を一つ削除するだけで、ツリーの半分が崩壊してしまいます。型を変更すれば、TypeScript が 3 つのディレクトリにわたってエラーを吐き出します。React Context API は、こうした中間役を完全に排除するために存在します。
Prop Drilling の実態
標準的なアプリのシェルを想像してみてください。現在のユーザーを取得する App コンポーネントがあります。App の中には Layout があり、Layout の中には Sidebar があり、Sidebar の中には Navigation がネストされ、最後に Navigation の中に、実際にユーザーオブジェクトを必要とする UserAvatar があります。
あなたのコードは次のようになります:
function App() {
const user = { name: 'Aarav', role: 'admin' };
return <Layout user={user} />;
}
function Layout({ user }) {
return <Sidebar user={user} />;
}
function Sidebar({ user }) {
return <Navigation user={user} />;
}
function Navigation({ user }) {
return <UserAvatar user={user} />;
}
Layout、Sidebar、Navigation は、そのユーザーオブジェクトを下に渡す以外に何もしていません。自分のものではない props が蓄積され、インターフェースは肥大化し、テスト時には一度も触れることのないデータをモック化する必要があります。本当の罪は、これが広まる速さです。isLoggedIn フラグ、locale 文字列、あるいは theme の値を追加するたびに、同じ行列が繰り返されます。
Context API がどのようにゲームチェンジャーとなるか
Context API を WiFi ルーターのようなものだと考えてください。各デバイスに届くようにすべての部屋に長いケーブルを引く代わりに、ルーターは空中に信号を飛ばします。範囲内のデバイスならどれでも直接接続できます。React の言葉で言えば、ルーターが Provider であり、信号が状態(state)やデータであり、デバイスが useContext を呼び出す任意のネストされたコンポーネントです。
セットアップには 3 つの要素があります:
React.createContext()がデータチャネルを構築します。- Provider がツリーの一部をラップし、値を送信します。
useContextフックを使うと、中間にある props を介さずに、子孫コンポーネントがその値を受け取れます。
ツリーの構造は維持されますが、ルートとリーフ(末端)の間にある枝は、もはや「運び屋の契約」に同意する必要はありません。
Context をゼロから構築する
ほとんどのアプリはいつかライトモードかダークモードを必要とするため、テーマ設定を用いた具体的な例を作成してみましょう。
まず、コンテキストオブジェクトを作成します。これがパイプになります:
import { createContext, useState, useMemo } from 'react';
const ThemeContext = createContext(null);
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
export default ThemeContext;
次に、アプリケーションを Provider でラップします。通常、これはルートの近くで行われます:
import { ThemeProvider } from './ThemeContext';
function App() {
return (
<ThemeProvider>
<Layout />
</ThemeProvider>
);
}
これで、どの子孫コンポーネントも直接信号を受け取ることができます。UI の深い場所に埋め込まれたトグルボタンの例です:
import { useContext } from 'react';
import ThemeContext from './ThemeContext';
function ThemeToggle() {
const { theme, setTheme } = useContext(ThemeContext);
return (
<button
onClick={() => setTheme(prev => prev === 'light' ? 'dark' : 'light')}
>
Current theme: {theme}
</button>
);
}
Layout、Sidebar、Navigation は theme prop を一度も見ることがないことに注目してください。これらは通常通りレンダリングされ、ThemeToggle はコンテキストから直接必要なものを取得します。配線は外部からは見えません。これこそがまさに狙いなのです。
Context が適している場所
Context は、多くの離れたコンポーネントが共有しているものの、単一の親が綺麗に所有しているわけではないデータに最適です。適した候補には以下が含まれます:
- テーマ設定: ライトモードやダークモード、アクセントカラー、フォントサイズなど。
- 認証状態: 現在のユーザーオブジェクト、ログイン状態、セッションの有効期限など。
- 言語とロケール: 国際化のための設定。
- ショッピングカートのデータ: ヘッダーのバッジ、ミニカートのドロップダウン、チェックアウトページの間で同期を維持する必要があるもの。
すべてのローカルな状態を Context に詰め込みたくなる衝動を抑えてください。2 つ下の階層にあるフォーム入力に、グローバルなブロードキャストは必要ありません。Context は真の横断的な関心事のために残しておき、それ以外は通常の props として扱わせましょう。
パフォーマンスの罠とその回避方法
Context は無料ではありません。コンテキストの値が更新されると、そのコンテキストに接続されているすべてのコンポーネントが再レンダリングされます。たとえ、それらが関心を持っている値の断片が変化していなくてもです。典型的な間違いは、親がレンダリングされるたびに Provider に新しいオブジェクトリテラルを渡してしまうことです。
テーマの例では、自身の親が更新されたために ThemeProvider が再レンダリングされるたびに、式 { theme, setTheme } が新しいオブジェクトを作成します。React は新しい参照を検出し、すべてのコンシューマーが更新されます。テーマがめったに変わらないのにアプリの状態が頻繁に変わる場合、不要なレンダリングのコストを支払うことになります。
解決策は 2 つあります。
更新頻度によってコンテキストを分割する。 ログイン時に一度だけ変わる UserContext を、数秒ごとに更新される NotificationContext と同じ Provider で共有すべきではありません。静的なデータが揮発性のデータと同じ再レンダリングの波に乗らないよう、これらを分離して保持してください。
値がオブジェクトや配列の場合は、useMemo で値をラップする。 React に安定した参照を与えます:
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const value = useMemo(() => ({ theme, setTheme }), [theme]);
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}
Now the object identity only shifts when theme actually changes. Descendant components that care about the context but are shielded by React.memo further down will skip the work.
Mistakes That Waste Your Time
The two errors that still show up in production code are easy to prevent.
First, forgetting to export the context itself. If you only export the Provider wrapper and keep the context object private, a developer writing a new feature cannot call useContext without refactoring your module. Export the context so consumers can import both the provider and the consumer hook cleanly.
Second, calling useContext outside the corresponding Provider. If ThemeToggle renders in a branch of the tree that is not wrapped in ThemeProvider, the hook returns the default value passed to createContext, or undefined if you passed none. This leads to silent failures like cannot read property of undefined. You can guard against this by assigning a sensible default or throwing a clear error early in the hook call.
Context vs Redux: Keep It Simple
You do not always need Redux. For medium-sized projects, Context paired with useState or useReducer covers the bulk of state sharing. Redux shines when you need time-travel debugging, complex middleware, or global transactions that must roll back in sequence. If your entire state story is a user object, a theme string, and a cart array, a store library adds boilerplate you will never leverage.
That said, Context is not a full state management system on its own. It does not give you a single global snapshot, and it does not batch updates across unrelated contexts. Use it as a replacement for prop drilling, not as an operating system for your entire data layer.
The Real Takeaway
Stop threading props through components that do not care about them. Create focused contexts for the data that actually spans your tree, wrap providers high enough to cover the consumers, and always stabilize the value object when you are passing collections or functions. Context API keeps yourReact code direct: props stay local, global data travels wirelessly, and your component boundaries stay clean.
