Context API
Context API:コンポーネント間の「どこでもドア」
Section titled “Context API:コンポーネント間の「どこでもドア」”注意: このドキュメントはTypeScript(TSX)前提で説明しています。現在のReact開発では、TypeScriptの採用が標準となっており、型安全性によるバグの早期発見、IDEの補完機能、リファクタリングの安全性などの理由から、JSXよりもTSXが選定されることが一般的です。
Context APIは、**コンポーネント間でデータを共有するための「どこでもドア」**のような仕組みです。親コンポーネントから子コンポーネントへ、何階層も下にデータを渡す必要がある場合、propsを何度も受け渡しする「バケツリレー(Prop Drilling)」を避けることができます。
解決する課題: Prop Drilling(バケツリレー)の撲滅
Section titled “解決する課題: Prop Drilling(バケツリレー)の撲滅”Prop Drillingとは
Section titled “Prop Drillingとは”**Prop Drilling(プロップドリリング)**は、親コンポーネントから深くネストされた子コンポーネントにデータを渡す際、中間のコンポーネントを経由してpropsを何度も受け渡しする問題です。
❌ 問題のあるコード: Prop Drilling
// 親コンポーネントtype AppProps = {};
function App({}: AppProps) { const [user, setUser] = useState<User>({ id: '1', name: 'John Doe', email: 'john@example.com', });
return ( <div> <Header user={user} /> {/* 1階層目: userを渡す */} </div> );}
// 1階層目: userを受け取って、そのまま下に渡すだけtype HeaderProps = { user: User;};
function Header({ user }: HeaderProps) { return ( <header> <Navigation user={user} /> {/* 2階層目: userを渡す */} </header> );}
// 2階層目: userを受け取って、そのまま下に渡すだけtype NavigationProps = { user: User;};
function Navigation({ user }: NavigationProps) { return ( <nav> <UserMenu user={user} /> {/* 3階層目: userを渡す */} </nav> );}
// 3階層目: ようやくuserを使用type UserMenuProps = { user: User;};
function UserMenu({ user }: UserMenuProps) { return <div>Welcome, {user.name}!</div>;}問題点:
- 中間コンポーネントが不要なpropsを受け取る:
HeaderやNavigationはuserを使わないのに、受け取って渡すだけ - コードが冗長: 各階層で同じpropsを定義する必要がある
- 保守性が低い: propsの型定義を複数箇所で更新する必要がある
- 可読性が低い: どのコンポーネントが実際に
userを使用しているか分かりにくい
Context APIで解決
Section titled “Context APIで解決”✅ 解決されたコード: Context APIを使用
// Contextを作成const UserContext = createContext<User | undefined>(undefined);
// Providerで提供function App() { const [user, setUser] = useState<User>({ id: '1', name: 'John Doe', email: 'john@example.com', });
return ( <UserContext.Provider value={user}> <Header /> {/* userを渡す必要がない */} </UserContext.Provider> );}
// 中間コンポーネント: userを渡す必要がないfunction Header() { return ( <header> <Navigation /> {/* userを渡す必要がない */} </header> );}
// 中間コンポーネント: userを渡す必要がないfunction Navigation() { return ( <nav> <UserMenu /> {/* userを渡す必要がない */} </nav> );}
// 実際にuserを使用するコンポーネント: Contextから直接取得function UserMenu() { const user = useContext(UserContext); if (!user) return null;
return <div>Welcome, {user.name}!</div>;}メリット:
- 中間コンポーネントがシンプル: 不要なpropsを受け取る必要がない
- コードが簡潔: propsの受け渡しが不要
- 保守性が高い: Contextの型定義を1箇所で管理
- 可読性が高い: 実際に使用するコンポーネントで
useContextを呼び出すだけ
使い方3ステップ: (1)作成 (2)提供 (3)消費
Section titled “使い方3ステップ: (1)作成 (2)提供 (3)消費”Context APIの使い方は、3つのステップで構成されます。
ステップ1: Contextの作成
Section titled “ステップ1: Contextの作成”createContextを使用して、Contextを作成します。
import { createContext } from 'react';
// Contextの型定義type Theme = 'light' | 'dark';
type ThemeContextType = { theme: Theme; toggleTheme: () => void;};
// Contextの作成(初期値はundefined)const ThemeContext = createContext<ThemeContextType | undefined>(undefined);ポイント:
- 型定義を明示: TypeScriptでContextの型を定義する
- 初期値はundefined: Provider外で使用された場合のエラーハンドリングのため
ステップ2: Providerで提供
Section titled “ステップ2: Providerで提供”Context.Providerを使用して、Contextの値を提供します。
import { useState, ReactNode } from 'react';
// Providerコンポーネントfunction ThemeProvider({ children }: { children: ReactNode }) { const [theme, setTheme] = useState<Theme>('light');
const toggleTheme = () => { setTheme(prev => prev === 'light' ? 'dark' : 'light'); };
return ( <ThemeContext.Provider value={{ theme, toggleTheme }}> {children} </ThemeContext.Provider> );}ポイント:
- Providerで囲む: Contextを使用するコンポーネントを
Providerで囲む - valueプロパティ: Contextの値を
valueプロパティで渡す
ステップ3: useContextで消費
Section titled “ステップ3: useContextで消費”useContextを使用して、Contextの値を取得します。
import { useContext } from 'react';
// カスタムフック: useContextのラッパー(推奨)function useTheme() { const context = useContext(ThemeContext);
if (context === undefined) { throw new Error('useTheme must be used within a ThemeProvider'); }
return context;}
// 使用例function ThemedButton() { const { theme, toggleTheme } = useTheme();
return ( <button onClick={toggleTheme}> Current theme: {theme} </button> );}ポイント:
- カスタムフックでラップ:
useContextを直接使わず、カスタムフックでラップしてエラーハンドリングを行う - エラーハンドリング: Provider外で使用された場合にエラーを投げる
コード例: 完全な実装
Section titled “コード例: 完全な実装”以下は、Context APIを使用した完全な実装例です。
実践例1: テーマ管理
Section titled “実践例1: テーマ管理”import { createContext, useContext, useState, ReactNode } from 'react';
// ステップ1: Contextの作成type Theme = 'light' | 'dark';
type ThemeContextType = { theme: Theme; toggleTheme: () => void;};
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
// ステップ2: Providerで提供function ThemeProvider({ children }: { children: ReactNode }) { const [theme, setTheme] = useState<Theme>('light');
const toggleTheme = () => { setTheme(prev => prev === 'light' ? 'dark' : 'light'); };
return ( <ThemeContext.Provider value={{ theme, toggleTheme }}> {children} </ThemeContext.Provider> );}
// ステップ3: useContextで消費(カスタムフック)function useTheme() { const context = useContext(ThemeContext);
if (context === undefined) { throw new Error('useTheme must be used within a ThemeProvider'); }
return context;}
// 使用例function App() { return ( <ThemeProvider> <Header /> <Main /> </ThemeProvider> );}
function Header() { const { theme, toggleTheme } = useTheme();
return ( <header style={{ backgroundColor: theme === 'light' ? '#fff' : '#333' }}> <button onClick={toggleTheme}>Toggle Theme</button> </header> );}
function Main() { const { theme } = useTheme();
return ( <main style={{ backgroundColor: theme === 'light' ? '#f5f5f5' : '#222' }}> <p>Current theme: {theme}</p> </main> );}実践例2: 認証管理
Section titled “実践例2: 認証管理”import { createContext, useContext, useState, ReactNode } from 'react';
// ステップ1: Contextの作成type User = { id: string; name: string; email: string;};
type AuthContextType = { user: User | null; login: (email: string, password: string) => Promise<void>; logout: () => void; isLoading: boolean;};
const AuthContext = createContext<AuthContextType | undefined>(undefined);
// ステップ2: Providerで提供function AuthProvider({ children }: { children: ReactNode }) { const [user, setUser] = useState<User | null>(null); const [isLoading, setIsLoading] = useState<boolean>(false);
const login = async (email: string, password: string) => { setIsLoading(true); try { const response = await fetch('/api/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password }), });
if (!response.ok) { throw new Error('Login failed'); }
const userData = await response.json(); setUser(userData); } catch (error) { console.error('Login error:', error); throw error; } finally { setIsLoading(false); } };
const logout = () => { setUser(null); };
return ( <AuthContext.Provider value={{ user, login, logout, isLoading }}> {children} </AuthContext.Provider> );}
// ステップ3: useContextで消費(カスタムフック)function useAuth() { const context = useContext(AuthContext);
if (context === undefined) { throw new Error('useAuth must be used within an AuthProvider'); }
return context;}
// 使用例function App() { return ( <AuthProvider> <Header /> <Main /> </AuthProvider> );}
function Header() { const { user, logout } = useAuth();
return ( <header> {user ? ( <div> <span>Welcome, {user.name}!</span> <button onClick={logout}>Logout</button> </div> ) : ( <span>Please login</span> )} </header> );}
function Main() { const { user, isLoading } = useAuth();
if (isLoading) { return <div>Loading...</div>; }
if (!user) { return <div>Please login to continue</div>; }
return <div>Main content for {user.name}</div>;}実践例3: 複数のContextの組み合わせ
Section titled “実践例3: 複数のContextの組み合わせ”import { createContext, useContext, useState, ReactNode } from 'react';
// Theme Contexttype Theme = 'light' | 'dark';
type ThemeContextType = { theme: Theme; toggleTheme: () => void;};
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
function ThemeProvider({ children }: { children: ReactNode }) { const [theme, setTheme] = useState<Theme>('light');
const toggleTheme = () => { setTheme(prev => prev === 'light' ? 'dark' : 'light'); };
return ( <ThemeContext.Provider value={{ theme, toggleTheme }}> {children} </ThemeContext.Provider> );}
function useTheme() { const context = useContext(ThemeContext); if (context === undefined) { throw new Error('useTheme must be used within a ThemeProvider'); } return context;}
// Language Contexttype Language = 'ja' | 'en';
type LanguageContextType = { language: Language; setLanguage: (lang: Language) => void;};
const LanguageContext = createContext<LanguageContextType | undefined>(undefined);
function LanguageProvider({ children }: { children: ReactNode }) { const [language, setLanguage] = useState<Language>('ja');
return ( <LanguageContext.Provider value={{ language, setLanguage }}> {children} </LanguageContext.Provider> );}
function useLanguage() { const context = useContext(LanguageContext); if (context === undefined) { throw new Error('useLanguage must be used within a LanguageProvider'); } return context;}
// 使用例: 複数のProviderを組み合わせるfunction App() { return ( <ThemeProvider> <LanguageProvider> <Header /> <Main /> </LanguageProvider> </ThemeProvider> );}
function Header() { const { theme, toggleTheme } = useTheme(); const { language, setLanguage } = useLanguage();
return ( <header> <button onClick={toggleTheme}>Theme: {theme}</button> <button onClick={() => setLanguage(language === 'ja' ? 'en' : 'ja')}> Language: {language} </button> </header> );}
function Main() { const { theme } = useTheme(); const { language } = useLanguage();
return ( <main> <p>Theme: {theme}</p> <p>Language: {language}</p> </main> );}注意点: パフォーマンスへの影響
Section titled “注意点: パフォーマンスへの影響”Contextの値が変わると、全コンポーネントが再レンダリングされる
Section titled “Contextの値が変わると、全コンポーネントが再レンダリングされる”重要な注意点: Contextの値が変わると、そのContextを購読している全コンポーネントが再レンダリングされます。これは、頻繁に更新される値(秒刻みのタイマーなど)には不向きです。
❌ 問題のあるコード: 頻繁に更新される値にContextを使用
// 問題: 秒刻みで更新されるタイマーをContextで管理type TimerContextType = { seconds: number;};
const TimerContext = createContext<TimerContextType | undefined>(undefined);
function TimerProvider({ children }: { children: ReactNode }) { const [seconds, setSeconds] = useState<number>(0);
useEffect(() => { const interval = setInterval(() => { setSeconds(prev => prev + 1); // 1秒ごとに更新 }, 1000);
return () => clearInterval(interval); }, []);
return ( <TimerContext.Provider value={{ seconds }}> {children} </TimerContext.Provider> );}
// 問題: TimerContextを購読している全コンポーネントが1秒ごとに再レンダリングされるfunction App() { return ( <TimerProvider> <Header /> {/* 1秒ごとに再レンダリングされる */} <Main /> {/* 1秒ごとに再レンダリングされる */} <Footer /> {/* 1秒ごとに再レンダリングされる */} </TimerProvider> );}問題点:
- 不要な再レンダリング:
secondsを使用していないコンポーネントも再レンダリングされる - パフォーマンスの低下: 頻繁な更新により、アプリケーション全体のパフォーマンスが低下する
- メモ化の効果が薄い:
React.memoを使用しても、Contextの値が変わるため再レンダリングされる
✅ 解決策: 頻繁に更新される値はContextで管理しない
// 解決策1: 必要なコンポーネントのみでuseStateを使用function TimerDisplay() { const [seconds, setSeconds] = useState<number>(0);
useEffect(() => { const interval = setInterval(() => { setSeconds(prev => prev + 1); }, 1000);
return () => clearInterval(interval); }, []);
return <div>Timer: {seconds}秒</div>;}
// 解決策2: Contextを分割する(値と更新関数を分離)type TimerValueContextType = { seconds: number;};
type TimerActionsContextType = { reset: () => void;};
const TimerValueContext = createContext<TimerValueContextType | undefined>(undefined);const TimerActionsContext = createContext<TimerActionsContextType | undefined>(undefined);
function TimerProvider({ children }: { children: ReactNode }) { const [seconds, setSeconds] = useState<number>(0);
useEffect(() => { const interval = setInterval(() => { setSeconds(prev => prev + 1); }, 1000);
return () => clearInterval(interval); }, []);
const reset = () => { setSeconds(0); };
return ( <TimerValueContext.Provider value={{ seconds }}> <TimerActionsContext.Provider value={{ reset }}> {children} </TimerActionsContext.Provider> </TimerValueContext.Provider> );}
// 値のみが必要なコンポーネントfunction TimerDisplay() { const context = useContext(TimerValueContext); if (!context) return null;
return <div>Timer: {context.seconds}秒</div>;}
// 更新関数のみが必要なコンポーネント(再レンダリングされない)function TimerResetButton() { const context = useContext(TimerActionsContext); if (!context) return null;
return <button onClick={context.reset}>Reset</button>;}Contextを使用すべき場合と使用すべきでない場合
Section titled “Contextを使用すべき場合と使用すべきでない場合”✅ Contextを使用すべき場合:
- グローバルな状態: テーマ、認証情報、言語設定など、アプリケーション全体で共有される状態
- 更新頻度が低い: ユーザーの操作やイベントに応じて更新される状態
- 深いネスト: 何階層も下のコンポーネントにデータを渡す必要がある場合
❌ Contextを使用すべきでない場合:
- 頻繁に更新される値: 秒刻みのタイマー、アニメーションの値など
- ローカルな状態: コンポーネント内でのみ使用される状態(
useStateで十分) - サーバー状態: APIから取得したデータ(React QueryやSWRなどの専用ライブラリを使用)
ベストプラクティス
Section titled “ベストプラクティス”- Contextを分割する: 値と更新関数を別のContextに分離することで、不要な再レンダリングを防ぐ
- Providerの範囲を最小限に: 必要なコンポーネントのみをProviderで囲む
- メモ化を検討する:
React.memoやuseMemoを使用して、不要な再レンダリングを防ぐ - カスタムフックでラップする:
useContextを直接使わず、カスタムフックでラップしてエラーハンドリングを行う
Context APIの3ステップ
Section titled “Context APIの3ステップ”- 作成:
createContextでContextを作成 - 提供:
Context.Providerで値を提供 - 消費:
useContext(カスタムフックでラップ)で値を取得
使用すべき場合
Section titled “使用すべき場合”- ✅ グローバルな状態(テーマ、認証など)
- ✅ 更新頻度が低い値
- ✅ 深いネストでのデータ共有
使用すべきでない場合
Section titled “使用すべきでない場合”- ❌ 頻繁に更新される値(秒刻みのタイマーなど)
- ❌ ローカルな状態(
useStateで十分) - ❌ サーバー状態(React Queryなどの専用ライブラリを使用)
- ⚠️ Contextの値が変わると、購読している全コンポーネントが再レンダリングされる
- ⚠️ 頻繁に更新される値には不向き
- ⚠️ Contextを分割することで、不要な再レンダリングを防ぐ
Context APIは、Prop Drillingを解決する強力なツールですが、適切に使用することが重要です。