React基礎講座 #12 useContext
前回は兄弟コンポーネントがデータを共有するために共通の親へ state をリフトアップするパターンを学びました。良いツールですが、コンポーネントツリーが深くなると一つ問題が起きます。今回はその問題と解決策である 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: 'チョルス', 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 一つで「共有 state とその操作方法」を一気に公開できます。
Provider をコンポーネントで包む #
Context の利用が増えてくると、Provider 自体を別のコンポーネントとして分離するのがすっきりします。状態管理ロジックを一箇所にまとめておく効果があります。
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 で扱うと、パフォーマンス問題が発生する可能性があります。
3. Context はグローバル状態ライブラリではない #
Context は「データ伝達経路」であり、それ自体で精緻な状態管理ツールではありません。アプリ全体の複雑な状態 (グローバルなユーザー情報 + 通知 + カート + 設定 + …) を扱うなら、Zustand、Redux Toolkit、Jotai のような専用ライブラリのほうが向いています。Context は小さな範囲の共有状態や、テーマ/言語/ユーザーといった「ほとんど変わらない」グローバルデータに適しています。
自分でやってみる #
テーマ (ライト/ダーク) を 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>1枚目のカード</h2>
<p>テーマを変えると色が変わります。</p>
</Card>
<Card>
<h2>2枚目のカード</h2>
<p>2枚のカードが同じテーマを共有しています。</p>
</Card>
</ThemeProvider>
);
}
export default App;ボタンを押すと、2枚のカードの色が同時に変わります。Card と ThemeToggle は互いの存在を知らず、親も2つの間の props を仲介していないにもかかわらず、同じテーマ状態を共有しています。prop drilling なしに、ツリーのどこからでも同じデータにアクセスできるようになったのです。
まとめ #
今回の記事では prop drilling 問題と解決策である Context API を学びました。整理すると:
- prop drilling — 中間のコンポーネントたちが、無関係な props をただ渡すだけの状況
- Context はツリーのどこかにある値を、その下の子孫が直接取り出して使える「経路」
- 利用の3ステップ:
createContext→<Provider value={...}>→useContext - 値と setter をオブジェクトでまとめて供給するパターンがよく使われる
- Provider のロジックは別コンポーネントに、消費はカスタムフックで包むときれい
- 単純な prop の伝達はただの props のほうが良く、よく変わるデータには不適切
ここまでがバッチ2 (#9〜#12) の終わりです。フォーム、useEffect、stateのリフトアップ、Context まで扱ったので、これで小さな実戦アプリを最初から最後まで作るのに必要なパターンはほぼすべて揃いました。
次の記事「React基礎講座 #13 カスタムフック」では、コンポーネント間でロジックを共有する最もエレガントなツール、カスタムフックを扱っていきます。今回の記事で少し使った useTheme も、実はカスタムフックの一例だったのですね。次の記事で本格的に扱っていきましょう。