useContext — 向いている場合と向いていない場合
prop drilling を解決する Context API。使い方の3ステップ・value 分離パターン・外部の状態ライブラリとの境目まで扱います。
11章で、兄弟コンポーネントがデータを共有するために共通の親へ state を持ち上げるパターンを扱いました。よい道具ですが、コンポーネントツリーが深くなるとひとつ問題が出てきます。本章ではその問題と、解決策である Context を扱い、Context の限界の向こうにある外部の状態ライブラリとの境目もあわせて整理します。
Prop Drilling 問題 #
次のようなコンポーネントツリーを想像してみてください。
App (state: user)
└── Layout
└── Sidebar
└── ProfileMenu
└── UserAvatar (ここで user 情報が必要)user state は App にあるのに、その値を実際に使うのは奥にある UserAvatar です。間にある Layout、Sidebar、ProfileMenu は user に関心がありませんが、下に渡すために props を受け取らなければなりません。
<Layout user={user}>
<Sidebar user={user}>
<ProfileMenu user={user}>
<UserAvatar user={user} />
</ProfileMenu>
</Sidebar>
</Layout>このように 間にあるコンポーネントが自分とは無関係な props を受け取り、ただ下に渡すだけの状況 を prop drilling(プロップドリリング) と呼びます。深さが深くなったり伝える値が増えると、コードがあっという間に散らかります。
React はこの問題を解決するために Context API という道具を提供します。
Context のアイデア #
Context の核心のアイデアはシンプルです。
コンポーネントツリーのどこかにデータを「供給」しておけば、その下のどんな深さの子孫からでも直接「購読」して取り出して使える。
間のコンポーネントを経由せずに、上から下にデータが 瞬間移動 するイメージです。
Context 使用の3ステップ #
Context は次の3ステップで使います。
- Context を作る —
createContextで作成 - 供給 —
<Context.Provider value={...}>でツリーのどこかを囲んでデータを提供 - 購読 — 子コンポーネントで
useContext(Context)で値を取り出して使う
上の user の例を Context で書き直してみます。
ステップ1 — Context の作成 #
import { createContext } from 'react';
export const UserContext = createContext(null);createContext に渡す値は デフォルト値 です。Provider に囲まれていない位置で useContext を呼んだときに使われる値です。
ステップ2 — Provider で供給 #
import { useState } from 'react';
import { UserContext } from './UserContext';
import Layout from './Layout';
function App() {
const [user, setUser] = useState({ name: 'Cheolsu', email: 'cheolsu@example.com' });
return (
<UserContext.Provider value={user}>
<Layout />
</UserContext.Provider>
);
}
export default App;UserContext.Provider で囲った領域の 中にあるすべての子孫コンポーネント が、value の値を取り出して使えるようになります。間にあるコンポーネントはもう user を props で受け取らなくてよくなります。
import Sidebar from './Sidebar';
function Layout() {
return (
<div>
<Sidebar />
</div>
);
}
export default Layout;import ProfileMenu from './ProfileMenu';
function Sidebar() {
return (
<aside>
<ProfileMenu />
</aside>
);
}
export default Sidebar;Layout、Sidebar、ProfileMenu は user について何も知る必要がありません。すっきりしました。
ステップ3 — useContext で購読 #
import { useContext } from 'react';
import { UserContext } from './UserContext';
function UserAvatar() {
const user = useContext(UserContext);
if (!user) return <p>ログインが必要です。</p>;
return (
<div>
<p>{user.name}</p>
<p>{user.email}</p>
</div>
);
}
export default UserAvatar;useContext(UserContext) を呼ぶと、もっとも近い UserContext.Provider が提供した値をそのまま受け取れます。間を経由せずに一度に取得しています。
値と関数を一緒に供給する #
Context の値はオブジェクトにして、state とその setter(または更新関数)を一緒に 入れるパターンが非常に多いです。そうすれば子孫から値を読むだけでなく変更もできます。
import { createContext } from 'react';
export const ThemeContext = createContext({
theme: 'light',
toggleTheme: () => {},
});import { useState, useCallback } from 'react';
import { ThemeContext } from './ThemeContext';
import Page from './Page';
function App() {
const [theme, setTheme] = useState('light');
const toggleTheme = useCallback(() => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
}, []);
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
<Page />
</ThemeContext.Provider>
);
}import { useContext } from 'react';
import { ThemeContext } from './ThemeContext';
function ThemeToggle() {
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<button onClick={toggleTheme}>
現在のテーマ: {theme}(クリックで切り替え)
</button>
);
}
export default ThemeToggle;子孫コンポーネントは theme(現在の値)と toggleTheme(変更関数)を一緒に取り出して使います。このパターンのおかげで、Context ひとつで「共有状態とその操作方法」をまとめて公開できます。
Provider をコンポーネントで包む #
Context の使用が増えると、Provider 自体を別のコンポーネントに分離する とすっきりします。状態管理ロジックを1か所にまとめる効果もあります。
import { useState, useCallback } from 'react';
import { ThemeContext } from './ThemeContext';
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = useCallback(() => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
}, []);
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
export default ThemeProvider;import ThemeProvider from './ThemeProvider';
import Page from './Page';
function App() {
return (
<ThemeProvider>
<Page />
</ThemeProvider>
);
}App がはるかにシンプルになりました。テーマ関連のすべてのロジックが ThemeProvider の中にカプセル化されていて、別の場所に持ち込みやすくもなります。
カスタムフックでもう一度包む #
useContext(ThemeContext) のように毎回 Context を直接 import するのも少し面倒です。よく使う Context は カスタムフックで包んで 使いやすくするパターンが多いです。
import { createContext, useContext } from 'react';
export const ThemeContext = createContext({
theme: 'light',
toggleTheme: () => {},
});
export function useTheme() {
return useContext(ThemeContext);
}import { useTheme } from './ThemeContext';
function ThemeToggle() {
const { theme, toggleTheme } = useTheme();
// ...
}消費する側のコードがぐっと短くなります。カスタムフックの本格的な話は次の 13章で扱います。
Context を乱用しない #
Context は強力ですが、あらゆる場所で使う道具ではありません。 次の点を覚えておいてください。
1. 単純な prop の伝達ならただの props のほうがよい #
親 - 子1〜2段階ならば props のほうがはるかに明示的で追跡しやすいです。深さが本当に深い(3〜4段階以上)か、複数の枝で一緒に使うデータのときに Context が活きます。
2. Provider の value が変わると、その下のすべての購読者が再レンダリングされる #
Context は子孫全体をひとまとめにしてしまうぶん、value が頻繁に変わると広範囲な再レンダリングが起きます。変更頻度の高いデータ(例:マウス座標)を Context で扱うとパフォーマンス問題が起きることがあります。
value 分離パターン で一部を緩和できます。頻繁に変わる値とほとんど変わらない値を、2つの Context に分けるやり方です。
// ほとんど変わらない — すべての子孫が購読しても負担が少ない
<UserContext.Provider value={user}>
{/* 頻繁に変わる — 購読範囲を狭めるほうがよい */}
<CursorContext.Provider value={cursorPosition}>
<App />
</CursorContext.Provider>
</UserContext.Provider>3. Context はグローバル状態ライブラリではない #
Context は「データ伝達のパイプライン」であって、それ自体で精緻な状態管理ツールではありません。アプリ全体の複雑な状態(グローバルなユーザー情報 + 通知 + カート + 設定など)を扱うなら、Zustand、Jotai、Redux Toolkit のような専用ライブラリのほうが合います。
3つのツールの用途を短く整理すると次のとおりです。
- Zustand — もっとも軽くてボイラープレートが少ないです。小〜中規模アプリで first pick
- Jotai — atom 単位で細かく分かれた状態。部分購読に強くパフォーマンス志向
- Redux Toolkit — 明示的な action / reducer 構造と devtools。大きなチームと複雑なドメインに適合
本書の1〜2部では外部のツールを導入しません。Context と lifting state up だけで解ける範囲が1〜2部の射程です。外部ツールの導入は本書では直接扱いませんが、付録 A(旧 React マイグレーション)で Redux-only → RSC + Server Actions + 小型 client store への移行手順を扱います。
やってみよう #
テーマ(ライト / ダーク)を Context で管理し、2つの子コンポーネントが同じテーマ状態を共有する例を作ってみます。
src/ThemeContext.js:
import { createContext, useContext, useState, useCallback } from 'react';
const ThemeContext = createContext({
theme: 'light',
toggleTheme: () => {},
});
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = useCallback(() => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
}, []);
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
return useContext(ThemeContext);
}src/Card.jsx:
import { useTheme } from './ThemeContext';
function Card({ children }) {
const { theme } = useTheme();
const styles = {
background: theme === 'light' ? '#fff' : '#222',
color: theme === 'light' ? '#000' : '#fff',
padding: '16px',
border: '1px solid #999',
borderRadius: '8px',
margin: '8px 0',
};
return <div style={styles}>{children}</div>;
}
export default Card;src/ThemeToggle.jsx:
import { useTheme } from './ThemeContext';
function ThemeToggle() {
const { theme, toggleTheme } = useTheme();
return (
<button onClick={toggleTheme}>
現在: {theme === 'light' ? '☀ ライト' : '🌙 ダーク'}(クリックで切り替え)
</button>
);
}
export default ThemeToggle;src/App.jsx:
import { ThemeProvider } from './ThemeContext';
import Card from './Card';
import ThemeToggle from './ThemeToggle';
function App() {
return (
<ThemeProvider>
<ThemeToggle />
<Card>
<h2>最初のカード</h2>
<p>テーマを切り替えると色が変わります。</p>
</Card>
<Card>
<h2>2番目のカード</h2>
<p>2つのカードは同じテーマを共有しています。</p>
</Card>
</ThemeProvider>
);
}
export default App;ボタンを押すと2つのカードの色が同時に変わります。Card と ThemeToggle は互いの存在を知らず、親も2つの間で props を仲介していないのに、同じテーマ状態を共有しています。prop drilling なしで ツリーのどこからでも同じデータにアクセスできるようになっています。
練習問題 #
- 上のテーマ例にダーク / ライト以外に「high-contrast」の3つ目のテーマを追加してみてください。
toggleThemeの代わりにsetTheme(value)を公開し、明示的に選べるようにします。Cardのスタイルにも3分岐すべて処理します。 - Context value 変更時の再レンダリング範囲を直接観察。子孫コンポーネントに
console.log('rendered')を仕込み、value を頻繁に変えてみてください。Provider の下のすべての子孫が一緒に再レンダリングされる様子を確認します。そのあと、頻繁に変わる値とほとんど変わらない値を2つの Context に分離すると、子孫の一部だけが再レンダリングされるのを確認してみます。 - 認証 Context パターン。
AuthContextとAuthProviderを作り、user/login(email, password)/logout()の3つを公開してください。ログイン時はモックで{ name: 'Cheolsu' }を setUser し、ログアウトはsetUser(null)です。LoginFormとUserBadgeの2つの子コンポーネントが同じ Context を購読し、一方でログインするともう一方が即座に更新されるか確認します。32章(認証とセッション)の土台になります。
一行まとめ: Context はツリーのどこかの値を、その下の子孫が直接取り出して使えるようにするパイプラインです。使い方は3ステップ —
createContext→<Provider value={...}>→useContext。値と setter をオブジェクトでまとめて供給するパターンが多く、Provider のロジックは別コンポーネントに、消費はカスタムフックで包むときれいです。単純な prop 伝達はただの props のほうがよく、頻繁に変わるデータには向きません。複雑なグローバル状態には Zustand / Jotai / Redux Toolkit のような外部ツールを使うほうがよいです。
次の章 #
本章で useTheme のような小さなカスタムフックをちらっと見ました。次の 13章 カスタムフックでは、コンポーネント間で ロジックを共有 するもっとも上品な道具であるカスタムフックを本格的に扱います。良いフックのインターフェイスの形、そして反対に「フックに切り出すべきでない場合」の基準も合わせて整理します。