Reactの基礎 - useContext
概要
useContext は、コンポーネントツリーを通じてデータを受け渡す仕組みである Context API から値を取得するためのHookである。
Reactアプリケーションでは、親から子へとデータをPropsで渡していく構造が基本となる。
しかし、コンポーネントの階層が深くなると、途中のコンポーネントがそのデータを使用しないにもかかわらず、下位のコンポーネントに渡すためだけにPropsを受け取り続ける Prop Drilling という問題が発生する。
useContext はこの問題を解消し、任意の階層からContextの値に直接アクセスできるようにする。
Context APIは、以下に示す3つの要素で構成される。
| 要素 | 説明 |
|---|---|
createContext |
コンテキストオブジェクトを作成する。 デフォルト値を引数に取る。 |
| Provider | 子コンポーネントに値を提供する。 値を変更すると、全ての購読コンポーネントが再レンダリングされる。 |
useContext |
Providerが提供する値をコンポーネントから取得する。 最も近い上位のProviderの値を返す。 |
React 19では、<MyContext.Provider value={...}> という構文に加えて、<MyContext value={...}> と記述する Context as Provider パターンが導入された。
また、条件分岐やループ内でも呼び出せる use() Hookが追加され、より柔軟なContext利用が可能になった。
Context APIの基本
Context APIを構成する3つの要素の使い方を以下に示す。
createContext
createContext 関数を使用してコンテキストオブジェクトを作成する。
引数にはProviderが存在しない場合に返されるデフォルト値を指定する。
import { createContext } from 'react';
// デフォルト値を指定してコンテキストを作成する
const ThemeContext = createContext('light');
// デフォルト値にオブジェクトを指定することもできる
const UserContext = createContext({ name: '未ログイン', isLoggedIn: false });
デフォルト値は、コンポーネントツリーのどこにも対応するProviderが存在しない場合にのみ使用される。
TypeScriptでの型安全なパターンについては、後述の#TypeScriptでの型定義セクションを参照すること。
Provider
Providerは、子孫コンポーネントにContextの値を提供するコンポーネントである。
Providerで囲まれたコンポーネントは、useContext を通じて値にアクセスできる。
React 18以前では、必ず MyContext.Provider という形式で記述する必要があった。
- React 18以前のProvider構文
function App() { return ( <ThemeContext.Provider value="dark"> <Header /> <Main /> </ThemeContext.Provider> ); }
- React 19以降のProvider構文 (Context as Provider)
- React 19以降では、コンテキストオブジェクト自体をProviderとして直接使用できる。
function App() {
return (
<ThemeContext value="dark">
<Header />
<Main />
</ThemeContext>
);
}
useContext
useContext Hookは、最も近い上位のProviderが提供する値を返す。
import { useContext } from 'react';
function Header() {
// 最も近いThemeContext.Providerのvalueを取得する
const theme = useContext(ThemeContext);
return (
<header className={theme === 'dark' ? 'header-dark' : 'header-light'}>
サイトヘッダ
</header>
);
}
Providerがネストしている場合、useContext は呼び出し元のコンポーネントから最も近い (最も内側の) Providerの値を返す。
TypeScriptでの型定義
TypeScriptを使用する場合、Contextの型を明示的に定義することで型安全なアクセスが可能になる。
Contextの型定義
デフォルト値に undefined を使用するパターンが推奨される。
このパターンにより、Providerの外でContextを使用した場合にTypeScriptが警告を出すようになる。
import { createContext, useState } from 'react';
interface ThemeContextValue {
theme: 'light' | 'dark';
setTheme: (theme: 'light' | 'dark') => void;
}
// デフォルト値をundefinedにして型パラメータを指定する
const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);
function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
カスタムHookによる型安全なアクセス
createContext<T | undefined>(undefined) パターンと組み合わせて、カスタムHookを作成することでより安全にContextを利用できる。
カスタムHookの中でundefinedチェックを行うことにより、Provider未配置時のエラーを早期に検出できる。
import { useContext } from 'react';
// ThemeContext用のカスタムHook
function useTheme(): ThemeContextValue {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error('useTheme は ThemeProvider の内側で使用してください');
}
return context;
}
// 使用側のコンポーネント
function ThemeToggleButton() {
const { theme, setTheme } = useTheme();
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
現在のテーマ: {theme}
</button>
);
}
このパターンを使用することにより、以下に示すメリットが得られる。
- 型安全性
- 戻り値が常に
ThemeContextValue型であることが保証される。
- 戻り値が常に
- エラーの早期検出
- Provider未配置の場合に明確なエラーメッセージが表示される。
- 利便性
- 使用側で毎回
useContextとContextオブジェクトをimportする必要がなくなる。
- 使用側で毎回
React 19でのContextの変更
React 19では、Contextに関する構文と機能が拡張された。
Context as Providerパターン
React 19以降では、コンテキストオブジェクトをProviderとして直接使用できる。
これにより、MyContext.Provider という冗長な記述が不要になる。
import { createContext, useState } from 'react';
const ThemeContext = createContext<'light' | 'dark'>('light');
function App() {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
return (
// React 19: <ThemeContext.Provider> の代わりに <ThemeContext> を直接使用できる
<ThemeContext value={theme}>
<Header />
<Main />
</ThemeContext>
);
}
use() Hook
React 19では、useContext の改良版として use() Hookが追加された。
use() の最大の特徴は、条件分岐やループの内側でも呼び出せる点である。
import { use } from 'react';
function ThemeDisplay({ showTheme }: { showTheme: boolean }) {
// use()は、if文の内側でも呼び出せる (通常のHookはこれができない)
if (showTheme) {
const theme = use(ThemeContext);
return <p>現在のテーマ: {theme}</p>;
}
return <p>テーマ非表示</p>;
}
下表に、useContext と use() の比較を示す。
| 項目 | useContext | use() |
|---|---|---|
| React バージョン | React 16.8以降 | React 19以降 |
| 条件分岐内での呼び出し | 不可 | 可能 |
| ループ内での呼び出し | 不可 | 可能 |
| Promiseの受け取り | 不可 | 可能 (Suspenseと組み合わせて使用) |
| 使用場所 | 関数コンポーネントのトップレベルのみ | より柔軟な場所で使用可能 |
サンプルコード
実務での使用頻度が高いContextのパターンを以下に示す。
テーマ切替
ライト/ダークモードのようなテーマ設定を、アプリケーション全体で共有するパターンである。
import { createContext, useContext, useState } from 'react';
type Theme = 'light' | 'dark';
interface ThemeContextValue {
theme: Theme;
resolvedTheme: Theme;
setTheme: (theme: Theme) => void;
toggleTheme: () => void;
}
const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<Theme>('light');
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, resolvedTheme: theme, setTheme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme(): ThemeContextValue {
const context = useContext(ThemeContext);
if (!context) throw new Error('useTheme は ThemeProvider の内側で使用してください');
return context;
}
// 使用例
function ThemeToggleButton() {
const { theme, toggleTheme } = useTheme();
return (
<button onClick={toggleTheme}>
{theme === 'light' ? 'ダークモードに切替' : 'ライトモードに切替'}
</button>
);
}
認証情報管理
ログインユーザの情報をアプリケーション全体で共有するパターンである。
import { createContext, useContext, useState } from 'react';
interface User {
id: string;
name: string;
email: string;
}
interface AuthContextValue {
user: User | null;
isLoading: boolean;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
}
const AuthContext = createContext<AuthContextValue | undefined>(undefined);
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(false);
const login = async (email: string, password: string) => {
setIsLoading(true);
try {
// 認証APIを呼び出す処理
const userData = await fetchUser(email, password);
setUser(userData);
}
finally {
setIsLoading(false);
}
};
const logout = () => {
setUser(null);
};
return (
<AuthContext.Provider value={{ user, isLoading, login, logout }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth(): AuthContextValue {
const context = useContext(AuthContext);
if (!context) throw new Error('useAuth は AuthProvider の内側で使用してください');
return context;
}
言語設定 (i18n)
アプリケーションの表示言語をグローバルに管理するパターンである。
import { createContext, useContext, useState, useCallback } from 'react';
type Locale = 'ja' | 'en' | 'zh';
type TranslationKey = 'greeting' | 'farewell' | 'submit';
const translations: Record<Locale, Record<TranslationKey, string>> = {
ja: { greeting: 'こんにちは', farewell: 'さようなら', submit: '送信' },
en: { greeting: 'Hello', farewell: 'Goodbye', submit: 'Submit' },
zh: { greeting: '你好', farewell: '再见', submit: '提交' },
};
interface I18nContextValue {
locale: Locale;
setLocale: (locale: Locale) => void;
t: (key: TranslationKey) => string;
}
const I18nContext = createContext<I18nContextValue | undefined>(undefined);
export function I18nProvider({ children }: { children: React.ReactNode }) {
const [locale, setLocale] = useState<Locale>('ja');
const t = useCallback((key: TranslationKey): string => {
return translations[locale][key];
}, [locale]);
return (
<I18nContext.Provider value={{ locale, setLocale, t }}>
{children}
</I18nContext.Provider>
);
}
export function useI18n(): I18nContextValue {
const context = useContext(I18nContext);
if (!context) throw new Error('useI18n は I18nProvider の内側で使用してください');
return context;
}
Providerのネスト
複数のContextを組み合わせる場合は、Providerをネストして使用する。
アプリケーションが成長すると、多数のProviderがネストされて記述が複雑になる問題が生じる。
これを解消するためのパターンとして、CombinedProviderパターンとComposeProvidersパターンがある。
// 複数のProviderを1つにまとめるCombinedProviderパターン
function CombinedProvider({ children }: { children: React.ReactNode }) {
return (
<ThemeProvider>
<AuthProvider>
<I18nProvider>
{children}
</I18nProvider>
</AuthProvider>
</ThemeProvider>
);
}
// App.tsxでの使用
function App() {
return (
<CombinedProvider>
<Router>
<AppContent />
</Router>
</CombinedProvider>
);
}
さらに汎用的なアプローチとして、Providerの配列を動的に合成するComposeProvidersパターンがある。
type ProviderComponent = ({ children }: { children: React.ReactNode }) => JSX.Element;
function composeProviders(...providers: ProviderComponent[]) {
return ({ children }: { children: React.ReactNode }) => {
return providers.reduceRight(
(acc, Provider) => <Provider>{acc}</Provider>,
children as JSX.Element
);
};
}
// 使用例
const AppProviders = composeProviders(ThemeProvider, AuthProvider, I18nProvider);
function App() {
return (
<AppProviders>
<AppContent />
</AppProviders>
);
}
パフォーマンスの考慮事項
Contextを使用する場合は、パフォーマンスへの影響を考慮する必要がある。
Context値変更時の再レンダリング
Contextの値が変更されると、そのContextを購読している全てのコンポーネントが再レンダリングされる。
オブジェクトや配列をContext値として渡す場合、参照が毎回変わるため不必要な再レンダリングが発生しやすい。
// 問題のあるパターン : レンダリングのたびに新しいオブジェクトが作成される
function ProblematicProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
return (
// value に毎回新しいオブジェクトが渡されるため、不要な再レンダリングが起きる
<UserContext.Provider value={{ user, setUser }}>
{children}
</UserContext.Provider>
);
}
useMemoによる最適化
useMemo を使用してContext値をメモ化することにより、不必要な再レンダリングを防ぐことができる。
import { createContext, useContext, useState, useMemo, useCallback } from 'react';
function OptimizedProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
// useCallback で関数参照を安定化する
const login = useCallback(async (email: string, password: string) => {
const userData = await fetchUser(email, password);
setUser(userData);
}, []);
const logout = useCallback(() => {
setUser(null);
}, []);
// useMemo でContext値をメモ化する
const value = useMemo(
() => ({ user, login, logout }),
[user, login, logout]
);
return (
<UserContext.Provider value={value}>
{children}
</UserContext.Provider>
);
}
Context分割パターン
1つの大きなContextに多くの値を格納すると、一部の値が変わるだけでそのContextを使用する全てのコンポーネントが再レンダリングされる。
更新頻度の異なるデータを別々のContextに分割することにより、再レンダリングの範囲を最小化できる。
// 分割前 : 1つの大きなContext
// theme が変わるだけで user を使うコンポーネントも再レンダリングされる
const AppContext = createContext<{ theme: Theme; user: User | null } | undefined>(undefined);
// 分割後 : 役割ごとに独立したContext
const ThemeContext = createContext<Theme | undefined>(undefined);
const UserContext = createContext<User | null | undefined>(undefined);
// StateとDispatchを分離するパターン (useReducerとの組み合わせで特に有効)
const StateContext = createContext<AppState | undefined>(undefined);
const DispatchContext = createContext<React.Dispatch<AppAction> | undefined>(undefined);
useContextとuseReducerの組み合わせ
useContext と useReducer を組み合わせることにより、ReduxのようなグローバルなFlux状態管理パターンを実現できる。
StateContextとDispatchContextを分離することで、状態を読み取るだけのコンポーネントはアクションを発行しても再レンダリングされず、パフォーマンスが向上する。
import { createContext, useContext, useReducer, useMemo } from 'react';
interface AppState {
count: number;
user: User | null;
}
type AppAction =
| { type: 'INCREMENT' }
| { type: 'DECREMENT' }
| { type: 'SET_USER'; payload: User | null };
function appReducer(state: AppState, action: AppAction): AppState {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + 1 };
case 'DECREMENT':
return { ...state, count: state.count - 1 };
case 'SET_USER':
return { ...state, user: action.payload };
default:
return state;
}
}
// StateとDispatchを別々のContextに分離する
const StateContext = createContext<AppState | undefined>(undefined);
const DispatchContext = createContext<React.Dispatch<AppAction> | undefined>(undefined);
const initialState: AppState = { count: 0, user: null };
export function AppStateProvider({ children }: { children: React.ReactNode }) {
const [state, dispatch] = useReducer(appReducer, initialState);
return (
<DispatchContext.Provider value={dispatch}>
<StateContext.Provider value={state}>
{children}
</StateContext.Provider>
</DispatchContext.Provider>
);
}
// 状態を読み取るカスタムHook
export function useAppState(): AppState {
const context = useContext(StateContext);
if (!context) throw new Error('useAppState は AppStateProvider の内側で使用してください');
return context;
}
// アクションを発行するカスタムHook
export function useAppDispatch(): React.Dispatch<AppAction> {
const context = useContext(DispatchContext);
if (!context) throw new Error('useAppDispatch は AppStateProvider の内側で使用してください');
return context;
}
よくある間違い
useContext を使用する時によく発生する誤りと対処法を以下に示す。
Provider未配置
useContext を呼び出したコンポーネントが、対応するProviderの外側に配置されている場合、createContext に渡したデフォルト値が返される。
デフォルト値が undefined の場合、プロパティアクセスでエラーが発生する。
// 誤り : ThemeProviderの外側でuseThemeを呼び出している
function App() {
return (
<div>
// ThemeToggleButtonは、ThemeProviderの外側にある
<ThemeToggleButton />
<ThemeProvider>
<Main />
</ThemeProvider>
</div>
);
}
// 正しい : ThemeProviderの内側に配置する
function App() {
return (
<ThemeProvider>
<ThemeToggleButton />
<Main />
</ThemeProvider>
);
}
また、コンポーネント関数の内側でProviderを返しても、そのコンポーネント自体はProviderの外側にあるため、自分自身はContextの値を受け取ることができない。
// 誤り : useTheme() は ThemeProvider より上の階層で呼び出されている
function ThemeAwareProvider({ children }: { children: React.ReactNode }) {
const { theme } = useTheme(); // この時点ではまだProviderが存在しない
return (
<ThemeProvider>
<div className={theme}>
{children}
</div>
</ThemeProvider>
);
}
不必要なContext使用
Contextは、アプリケーション全体 または 複数の深い階層に渡って、データを共有する場合に適したツールである。
1〜2階層のデータ受け渡しであれば、Propsを使用する方がシンプルでデータの流れが明確になる。
// 不必要なContextの例 : 1階層だけならPropsで十分
// Context使用 (過剰)
function Parent() {
return <UserNameContext.Provider value="田中"><Child /></UserNameContext.Provider>;
}
function Child() {
const name = useContext(UserNameContext);
return <p>{name}</p>;
}
// Propsを使用 (適切)
function Parent() {
return <Child name="田中" />;
}
function Child({ name }: { name: string }) {
return <p>{name}</p>;
}
下表に、PropsとContextの使い分けの目安を示す。
| 状況 | 推奨手段 |
|---|---|
| 1〜2階層のデータ受け渡し | Props |
| 多くのコンポーネントで必要なデータ | Context |
| コンポーネントツリーの深い階層へのデータ伝達 | Context |
| テーマ、認証、言語設定などのグローバルな状態 | Context |
| 局所的なコンポーネント状態 | useState / useReducer (Propsで渡す) |
関連情報