Skip to content

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(プロップドリリング)**は、親コンポーネントから深くネストされた子コンポーネントにデータを渡す際、中間のコンポーネントを経由して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>;
}

問題点:

  1. 中間コンポーネントが不要なpropsを受け取る: HeaderNavigationuserを使わないのに、受け取って渡すだけ
  2. コードが冗長: 各階層で同じpropsを定義する必要がある
  3. 保守性が低い: propsの型定義を複数箇所で更新する必要がある
  4. 可読性が低い: どのコンポーネントが実際にuserを使用しているか分かりにくい

✅ 解決されたコード: 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>;
}

メリット:

  1. 中間コンポーネントがシンプル: 不要なpropsを受け取る必要がない
  2. コードが簡潔: propsの受け渡しが不要
  3. 保守性が高い: Contextの型定義を1箇所で管理
  4. 可読性が高い: 実際に使用するコンポーネントでuseContextを呼び出すだけ

使い方3ステップ: (1)作成 (2)提供 (3)消費

Section titled “使い方3ステップ: (1)作成 (2)提供 (3)消費”

Context APIの使い方は、3つのステップで構成されます。

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外で使用された場合のエラーハンドリングのため

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プロパティで渡す

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外で使用された場合にエラーを投げる

以下は、Context APIを使用した完全な実装例です。

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>
);
}
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 Context
type 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 Context
type 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>
);
}

問題点:

  1. 不要な再レンダリング: secondsを使用していないコンポーネントも再レンダリングされる
  2. パフォーマンスの低下: 頻繁な更新により、アプリケーション全体のパフォーマンスが低下する
  3. メモ化の効果が薄い: 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を使用すべき場合:

  1. グローバルな状態: テーマ、認証情報、言語設定など、アプリケーション全体で共有される状態
  2. 更新頻度が低い: ユーザーの操作やイベントに応じて更新される状態
  3. 深いネスト: 何階層も下のコンポーネントにデータを渡す必要がある場合

❌ Contextを使用すべきでない場合:

  1. 頻繁に更新される値: 秒刻みのタイマー、アニメーションの値など
  2. ローカルな状態: コンポーネント内でのみ使用される状態(useStateで十分)
  3. サーバー状態: APIから取得したデータ(React QueryやSWRなどの専用ライブラリを使用)
  1. Contextを分割する: 値と更新関数を別のContextに分離することで、不要な再レンダリングを防ぐ
  2. Providerの範囲を最小限に: 必要なコンポーネントのみをProviderで囲む
  3. メモ化を検討する: React.memouseMemoを使用して、不要な再レンダリングを防ぐ
  4. カスタムフックでラップする: useContextを直接使わず、カスタムフックでラップしてエラーハンドリングを行う

  1. 作成: createContextでContextを作成
  2. 提供: Context.Providerで値を提供
  3. 消費: useContext(カスタムフックでラップ)で値を取得
  • ✅ グローバルな状態(テーマ、認証など)
  • ✅ 更新頻度が低い値
  • ✅ 深いネストでのデータ共有
  • ❌ 頻繁に更新される値(秒刻みのタイマーなど)
  • ❌ ローカルな状態(useStateで十分)
  • ❌ サーバー状態(React Queryなどの専用ライブラリを使用)
  • ⚠️ Contextの値が変わると、購読している全コンポーネントが再レンダリングされる
  • ⚠️ 頻繁に更新される値には不向き
  • ⚠️ Contextを分割することで、不要な再レンダリングを防ぐ

Context APIは、Prop Drillingを解決する強力なツールですが、適切に使用することが重要です。