useContextとContext API — propsを経由せずデータを渡す
この章の目次開く
- propsのバケツリレー — prop drilling
- Contextの3つの役割
- createContext — Contextを作る
- デフォルト値は更新されない
- Provider — 配下へ値を提供する
- useContext — Providerが渡した値を受け取る
- Providerが入れ子なら、内側の値が使われる
- useStateと組み合わせて値を更新する
- TypeScriptでは専用フックで安全に読む
- Contextによる再レンダー
- Contextを使う前に検討すること
- よくあるハマりどころ
- Providerを同じコンポーネントの下に置いている
- デフォルト値が更新されると思っている
- Contextを巨大なストアにしている
- まとめ
- 参考リンク
親から子へデータを渡す基本は props です。しかし、同じ値をツリーの深い場所にある複数のコンポーネントが必要とすると、途中のコンポーネントまで値を受け取り、次へ渡すだけのコードが増えていきます。
ReactのContext APIを使うと、上位のコンポーネントが提供した値を、その配下にあるコンポーネントから直接読み取れます。この章では、Contextを作る createContext、値を提供するProvider、値を読み取る useContext の3つを順番に整理します。
学習者propsを何段も渡すのが面倒なら、最初から全部Contextに入れればいいの?
便利だからこそ、使う範囲の見極めが重要です。まずはContextが解決する問題から見ていきましょう。
propsのバケツリレー — prop drilling
ログイン中のユーザー名を、ページの奥にある Avatar で表示する例を考えます。
function App() {
const userName = 'Ada';
return <Layout userName={userName} />;
}
function Layout({ userName }: { userName: string }) {
return <Header userName={userName} />;
}
function Header({ userName }: { userName: string }) {
return <Avatar userName={userName} />;
}
function Avatar({ userName }: { userName: string }) {
return <p>{userName}</p>;
}userName を実際に使うのは Avatar だけですが、Layout と Header もpropsを受け取って次へ渡しています。このように、値を必要な場所まで中継し続ける状態を prop drilling と呼びます。
小さなツリーならpropsの方がデータの流れを追いやすく、問題ありません。Contextが役立つのは、次の条件が重なる場面です。
- 同じ値を離れた複数のコンポーネントが必要とする
- 間にあるコンポーネントは、その値を使わず中継するだけ
- テーマ、ログインユーザー、言語設定など、サブツリー全体で共有する意味がある
サブツリーとは、あるコンポーネントを起点に、その配下にある子・孫コンポーネントをまとめた範囲のことです。上の例では、Layout を起点にすると、Layout とその下にある Header、Avatar が1つのサブツリーになります。「ツリーの一部分」を表す一般的な技術用語で、Reactの公式ドキュメントでも使われています。
Contextの3つの役割
Context APIは、作成・提供・読み取りの3段階で使います。
| 役割 | API | 行うこと |
|---|---|---|
| 作成 | createContext | 共有する情報の種類を表すContextオブジェクトを作る |
| 提供 | <SomeContext value={...}> | 配下のコンポーネントへ値を公開する |
| 読み取り | useContext(SomeContext) | 自分を囲んでいるProviderの value を受け取る |
Contextは値をどこにでも置けるグローバル変数ではありません。Providerで囲んだ範囲にだけ値を提供し、同じContextを読むコンポーネントだけがその値を受け取ります。
createContext — Contextを作る
まず、コンポーネントの外側で createContext を呼び出します。
構文: createContext(defaultValue)
| 引数 | 渡せるもの | 説明 |
|---|---|---|
defaultValue | 任意の値 | 上位に対応するProviderがない場合に返すフォールバック値。不要なら null を指定する |
戻り値: 値の提供と読み取りに使うContextオブジェクト
import { createContext } from 'react';
type Theme = 'light' | 'dark';
export const ThemeContext = createContext<Theme>('light');createContext はフックではなく、Contextオブジェクトを作るReact APIです。コンポーネントがレンダーされるたびに作り直さないよう、通常はモジュールのトップレベルで1回だけ呼び出します。
デフォルト値は更新されない
createContext('light') の 'light' は、Providerが見つからないときのフォールバックです。Providerの初期値ではなく、あとから自動で変わるstateでもありません。
const ThemeContext = createContext<'light' | 'dark'>('light');
function Button() {
// 上位にProviderがなければ 'light'
const theme = useContext(ThemeContext);
return <button className={theme}>保存</button>;
}テストや独立した部品で妥当なフォールバックがあるなら、実際の値を指定して構いません。一方、Providerなしで使うこと自体をエラーにしたい場合は、後述するように null を使います。
Provider — 配下へ値を提供する
React 19では、作成したContextオブジェクトをコンポーネントとしてレンダーし、value に共有したい値を渡します。
import { ThemeContext } from './ThemeContext';
export default function App() {
return (
<ThemeContext value="dark">
<Page />
</ThemeContext>
);
}Providerのprops:
| prop | 渡せるもの | 説明 |
|---|---|---|
value | 任意の値 | Provider配下で同じContextを読むコンポーネントへ渡す値 |
children | React要素 | Contextの値を利用できる子要素 |
レンダー結果: children をそのまま描画し、配下へ value を提供する
React 18以前のコードでは、次の .Provider 形式をよく見かけます。
<ThemeContext.Provider value="dark">
<Page />
</ThemeContext.Provider>React 19では <ThemeContext value="dark"> と短く書けます。既存プロジェクトを読むために両方の形を知っておくとよいでしょう。
useContext — Providerが渡した値を受け取る
Providerが提供した値は、配下のコンポーネントから useContext で読み取ります。
構文: useContext(SomeContext)
| 引数 | 渡せるもの | 説明 |
|---|---|---|
SomeContext | createContext の戻り値 | 読み取りたいContextオブジェクト |
戻り値: 自分を囲んでいるProviderの value(入れ子なら一番内側のもの)。Providerがなければ createContext の defaultValue
import { useContext } from 'react';
import { ThemeContext } from './ThemeContext';
export function SaveButton() {
const theme = useContext(ThemeContext);
return (
<button className={theme === 'dark' ? 'button-dark' : 'button-light'}>
保存
</button>
);
}useContext はフックなので、コンポーネントのトップレベルで呼び出します。条件分岐やループの中では呼び出せません。
Providerが入れ子なら、内側の値が使われる
同じContextのProviderはネストできます。その場合、読み取るコンポーネントを囲んでいるProviderのうち、一番内側のものの値が使われます。
<ThemeContext value="light">
<Header />
<ThemeContext value="dark">
<AdminPanel />
</ThemeContext>
</ThemeContext>この例では Header は 'light'、AdminPanel は 'dark' を読み取ります。ページ全体の設定を、一部のサブツリーだけ上書きしたいときに使えます。
先生useContextは「一番上の値」を探すのではなく、自分から親方向へたどって最初に見つかったProviderの値を読む、と考えよう。
useStateと組み合わせて値を更新する
Contextには固定値だけでなく、stateと更新関数をまとめて渡せます。Providerの value が変わると、そのContextを読んでいる配下のコンポーネントは新しい値で再レンダーされます。
import { createContext, useContext, useState } from 'react';
type Theme = 'light' | 'dark';
type ThemeContextValue = {
theme: Theme;
toggleTheme: () => void;
};
const ThemeContext = createContext<ThemeContextValue | null>(null);
function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<Theme>('light');
function toggleTheme() {
setTheme((current) => (current === 'light' ? 'dark' : 'light'));
}
return (
<ThemeContext value={{ theme, toggleTheme }}>
{children}
</ThemeContext>
);
}
function ThemeButton() {
const context = useContext(ThemeContext);
if (context === null) {
throw new Error('ThemeButtonはThemeProviderの内側で使用してください');
}
return <button onClick={context.toggleTheme}>現在: {context.theme}</button>;
}
export default function App() {
return (
<ThemeProvider>
<ThemeButton />
</ThemeProvider>
);
}stateそのものは ThemeProvider が所有しています。Contextは、そのstateと更新手段を配下へ届ける役割です。
TypeScriptでは専用フックで安全に読む
前の例のような null チェックを利用側で毎回書くと、コードが重複します。専用のカスタムフックへまとめると、Providerの置き忘れを早く発見できます。
import { createContext, useContext } from 'react';
type ThemeContextValue = {
theme: 'light' | 'dark';
toggleTheme: () => void;
};
const ThemeContext = createContext<ThemeContextValue | null>(null);
export function useTheme() {
const context = useContext(ThemeContext);
if (context === null) {
throw new Error('useThemeはThemeProviderの内側で使用してください');
}
return context;
}利用側はContextオブジェクトを直接意識せず、useTheme() からnullではない値を受け取れます。
function ThemeButton() {
const { theme, toggleTheme } = useTheme();
return <button onClick={toggleTheme}>現在: {theme}</button>;
}この形には次の利点があります。
- 利用側でnullチェックを繰り返さなくてよい
- Providerの外で呼び出したとき、原因が分かるエラーを出せる
- Contextの読み取り方法を1か所にまとめられる
Contextによる再レンダー
Providerの value が変わると、そのContextを読んでいるコンポーネントは再レンダーされます。value の比較には Object.is が使われるため、オブジェクトや関数を毎回新しく作ると、内容が同じでも別の値と判定されます。
function AppProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
// AppProviderが再レンダーされるたび、新しいオブジェクトになる
const value = { user, setUser };
return <UserContext value={value}>{children}</UserContext>;
}小規模な画面で最初から最適化する必要はありません。ただし、Contextに無関係な値まで大量に詰め込むと、1項目の変更で多くの利用側が再レンダーされます。
対策の基本は次のとおりです。
- 役割が異なる値は別のContextへ分ける
- Providerを必要な範囲まで下げる
- 大きなオブジェクトを何でも1つのContextへ集約しない
- 実測して問題がある場合に、
useMemoやuseCallbackで参照を安定させる
useMemo と useCallback は後の章で詳しく扱います。まずはContextの責務を小さく保つことを優先してください。
Contextを使う前に検討すること
Contextを使うとpropsの記述は減りますが、値がどこから来たのかは見えにくくなります。次の順番で考えると、過剰なContextを避けられます。
- 近い親子間ならpropsで渡す
- 中間コンポーネントがレイアウトだけを担当するなら、
childrenとしてJSXを渡せないか考える - 離れた複数箇所が同じ値を必要とするならContextを検討する
| 状況 | 適した方法 |
|---|---|
| 親から直接の子へ値を渡す | props |
| UIの入れ子構造を組み替える | children |
| サブツリー内の離れた複数箇所で共有する | Context |
| サーバーから取得したデータをキャッシュ・再取得する | データ取得ライブラリやフレームワークの仕組みも検討 |
テーマ、現在のユーザー、言語、ルーティング情報のように「ツリー内の広い範囲で同じ意味を持つ値」はContextと相性がよいです。一方、1つのフォーム内だけで使う入力値や、1組の親子だけが使う値までContextにすると、かえって追いにくくなります。
よくあるハマりどころ
Providerを同じコンポーネントの下に置いている
useContext が読むのは、呼び出したコンポーネントより上にあるProviderです。同じコンポーネントがreturnするProviderは、そのコンポーネント自身の useContext には影響しません。
// NG — useContextを呼んだ時点では上位にProviderがない
function App() {
const theme = useContext(ThemeContext);
return (
<ThemeContext value="dark">
<p>{theme}</p>
</ThemeContext>
);
}Providerをさらに上へ移すか、値を読む処理を子コンポーネントへ分けます。
function ThemeText() {
const theme = useContext(ThemeContext);
return <p>{theme}</p>;
}
function App() {
return (
<ThemeContext value="dark">
<ThemeText />
</ThemeContext>
);
}デフォルト値が更新されると思っている
createContext に渡す値は静的なフォールバックです。変更可能な値を共有したいなら、Providerの上で useState を使い、そのstateを value に渡します。
Contextを巨大なストアにしている
アプリの全状態を1つのContextへ入れると、更新の影響範囲が広がり、どの変更で何が再レンダーされるのか分かりにくくなります。「認証」「テーマ」「言語」のように責務で分け、Providerの範囲も必要最小限にします。
まとめ
createContext は共有する情報の種類を表すContextオブジェクトを作り、Providerはその配下へ値を提供します。useContext は、自分を囲んでいるProvider(入れ子なら一番内側)の value を受け取り、その値が変わると再レンダーされます。
Contextはprop drillingを減らせますが、propsの完全な代替ではありません。まずpropsや children を検討し、サブツリー内の離れた複数コンポーネントが同じ値を必要とするときに使います。
次の章では、値を保持しても再レンダーを引き起こさない useRef を扱います。