React useContext入門|グローバルな状態管理を簡単に

React useContextの基本的な使い方から実践的な活用方法まで詳しく解説。グローバルな状態管理を簡単に実装する方法を学べます。

Learning Next 運営
43 分で読めます

みなさん、Reactの状態管理で困ったことはありませんか?

「コンポーネントが深くなるとpropsの受け渡しが大変」 「ユーザー情報を色々な場所で使いたいけど、どうすればいいの?」 「ReduxやZustandは難しそう...」

こんなふうに思ったことはありませんか?

そんな時に役立つのが、React useContextです。 この記事では、useContextを使った簡単なグローバル状態管理について、基本から実践的な使い方まで詳しく解説します。

コードが複雑になりがちな状態管理を、スッキリと整理する方法を一緒に学んでいきましょう。

useContextって何?

useContextは、Reactでグローバルな状態を管理するためのフックです。

簡単に言うと、データの倉庫を作って、どのコンポーネントからでもアクセスできる仕組みなんです。

propsの受け渡し問題

まず、useContextがなぜ便利なのか確認してみましょう。

// ❌ propsのバケツリレー(深いコンポーネント階層)
function App() {
  const [user, setUser] = useState({ name: '田中太郎' });
  
  return <Dashboard user={user} setUser={setUser} />;
}

function Dashboard({ user, setUser }) {
  return <Sidebar user={user} setUser={setUser} />;
}

function Sidebar({ user, setUser }) {
  return <UserMenu user={user} setUser={setUser} />;
}

function UserMenu({ user, setUser }) {
  return <UserProfile user={user} setUser={setUser} />;
}

function UserProfile({ user, setUser }) {
  return <div>{user.name}</div>;
}

このように、propsを何層にも渡していくのは大変ですよね。 useContextを使うと、この問題をスッキリ解決できます。

useContextの3つの要素

useContextは3つの要素で成り立っています。

  • Context: データを保管する「箱」
  • Provider: データを提供する「配達員」
  • useContext: データを受け取る「受信機」

イメージとしては、データ放送局みたいな感じです。 Provider(放送局)がデータを送信して、useContext(ラジオ)でそれを受信するんです。

useContextの利点

useContextを使うメリットはたくさんあります。

  • propsの受け渡しが不要になる
  • コードがスッキリして読みやすくなる
  • データの管理が一箇所に集約される
  • 再利用性が高いコンポーネントが作れる

でも大丈夫です! 使い方は思っているより簡単ですよ。

基本的な使い方

useContextの基本的な使い方を見てみましょう。

シンプルなユーザー管理

まずは、ユーザー情報を管理する簡単な例から始めます。

import React, { createContext, useContext, useState } from 'react';

// 1. Context の作成
const UserContext = createContext();

// 2. Provider コンポーネントの作成
function UserProvider({ children }) {
  const [user, setUser] = useState({
    name: '田中太郎',
    email: 'tanaka@example.com',
    role: 'admin'
  });

  return (
    <UserContext.Provider value={{ user, setUser }}>
      {children}
    </UserContext.Provider>
  );
}

// 3. useContext を使用するコンポーネント
function UserProfile() {
  const { user } = useContext(UserContext);

  return (
    <div>
      <h2>ユーザープロフィール</h2>
      <p>名前: {user.name}</p>
      <p>メールアドレス: {user.email}</p>
      <p>役割: {user.role}</p>
    </div>
  );
}

function UserSettings() {
  const { user, setUser } = useContext(UserContext);

  const handleNameChange = (newName) => {
    setUser(prev => ({ ...prev, name: newName }));
  };

  return (
    <div>
      <h2>ユーザー設定</h2>
      <input
        type="text"
        value={user.name}
        onChange={(e) => handleNameChange(e.target.value)}
        placeholder="名前を入力"
      />
    </div>
  );
}

// 4. アプリケーションで使用
function App() {
  return (
    <UserProvider>
      <div>
        <h1>アプリケーション</h1>
        <UserProfile />
        <UserSettings />
      </div>
    </UserProvider>
  );
}

この例では、UserProviderの中にあるコンポーネントなら、どこからでもuserにアクセスできます。 propsを渡す必要がないので、とてもスッキリしていますね。

カスタムフックで使いやすく

もう少し使いやすくするために、カスタムフックを作ってみましょう。

import React, { createContext, useContext, useState } from 'react';

const UserContext = createContext();

// カスタムフック
function useUser() {
  const context = useContext(UserContext);
  
  if (!context) {
    throw new Error('useUser must be used within a UserProvider');
  }
  
  return context;
}

function UserProvider({ children }) {
  const [user, setUser] = useState({
    name: '田中太郎',
    email: 'tanaka@example.com',
    role: 'admin'
  });

  const updateUser = (updates) => {
    setUser(prev => ({ ...prev, ...updates }));
  };

  const resetUser = () => {
    setUser({
      name: '',
      email: '',
      role: 'user'
    });
  };

  return (
    <UserContext.Provider value={{ 
      user, 
      setUser, 
      updateUser, 
      resetUser 
    }}>
      {children}
    </UserContext.Provider>
  );
}

// 使用例
function UserProfile() {
  const { user } = useUser(); // カスタムフックを使用

  return (
    <div>
      <h2>ユーザープロフィール</h2>
      <p>名前: {user.name}</p>
      <p>メールアドレス: {user.email}</p>
      <p>役割: {user.role}</p>
    </div>
  );
}

function UserSettings() {
  const { user, updateUser, resetUser } = useUser();

  return (
    <div>
      <h2>ユーザー設定</h2>
      <input
        type="text"
        value={user.name}
        onChange={(e) => updateUser({ name: e.target.value })}
        placeholder="名前を入力"
      />
      <button onClick={resetUser}>リセット</button>
    </div>
  );
}

カスタムフックuseUserを作ることで、以下のメリットがあります。

  • エラーハンドリングが自動でできる
  • 使いやすいAPIを提供できる
  • コードの重複を避けられる

とても便利ですね!

実践的な活用例

実際のアプリでよく使われるパターンを見てみましょう。

テーマ管理システム

ダークモードとライトモードの切り替えを管理してみます。

import React, { createContext, useContext, useState, useEffect } from 'react';

const ThemeContext = createContext();

function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme must be used within a ThemeProvider');
  }
  return context;
}

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');

  // ローカルストレージから設定を読み込み
  useEffect(() => {
    const savedTheme = localStorage.getItem('theme');
    if (savedTheme) {
      setTheme(savedTheme);
    }
  }, []);

  // テーマ変更時にローカルストレージに保存
  useEffect(() => {
    localStorage.setItem('theme', theme);
    document.documentElement.setAttribute('data-theme', theme);
  }, [theme]);

  const toggleTheme = () => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light');
  };

  const themes = {
    light: {
      backgroundColor: '#ffffff',
      color: '#333333',
      primaryColor: '#007bff',
      secondaryColor: '#6c757d'
    },
    dark: {
      backgroundColor: '#1a1a1a',
      color: '#ffffff',
      primaryColor: '#0d6efd',
      secondaryColor: '#6c757d'
    }
  };

  return (
    <ThemeContext.Provider value={{
      theme,
      setTheme,
      toggleTheme,
      colors: themes[theme]
    }}>
      {children}
    </ThemeContext.Provider>
  );
}

// テーマ切り替えボタン
function ThemeToggle() {
  const { theme, toggleTheme } = useTheme();

  return (
    <button onClick={toggleTheme}>
      {theme === 'light' ? '🌙' : '☀️'} 
      {theme === 'light' ? 'ダークモード' : 'ライトモード'}
    </button>
  );
}

// テーマを適用したコンポーネント
function ThemedCard({ title, children }) {
  const { colors } = useTheme();

  return (
    <div style={{
      backgroundColor: colors.backgroundColor,
      color: colors.color,
      border: `1px solid ${colors.secondaryColor}`,
      padding: '20px',
      borderRadius: '8px',
      margin: '10px 0'
    }}>
      <h3 style={{ color: colors.primaryColor }}>{title}</h3>
      {children}
    </div>
  );
}

この例では、テーマの情報をローカルストレージに保存しています。 ページを更新しても、設定が残るので便利ですね。

認証システム

ユーザーのログイン状態を管理する実用的な例も見てみましょう。

import React, { createContext, useContext, useState, useEffect } from 'react';

const AuthContext = createContext();

function useAuth() {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth must be used within an AuthProvider');
  }
  return context;
}

function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  // アプリケーション起動時に認証状態を確認
  useEffect(() => {
    const checkAuth = async () => {
      try {
        const token = localStorage.getItem('authToken');
        if (token) {
          // トークンを使ってユーザー情報を取得
          const response = await fetch('/api/user', {
            headers: { Authorization: `Bearer ${token}` }
          });
          
          if (response.ok) {
            const userData = await response.json();
            setUser(userData);
          } else {
            localStorage.removeItem('authToken');
          }
        }
      } catch (error) {
        console.error('認証確認エラー:', error);
        localStorage.removeItem('authToken');
      } finally {
        setLoading(false);
      }
    };

    checkAuth();
  }, []);

  const login = async (email, password) => {
    try {
      const response = await fetch('/api/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email, password })
      });

      if (response.ok) {
        const data = await response.json();
        localStorage.setItem('authToken', data.token);
        setUser(data.user);
        return { success: true };
      } else {
        const error = await response.json();
        return { success: false, error: error.message };
      }
    } catch (error) {
      return { success: false, error: 'ログインに失敗しました' };
    }
  };

  const logout = () => {
    localStorage.removeItem('authToken');
    setUser(null);
  };

  return (
    <AuthContext.Provider value={{
      user,
      loading,
      login,
      logout,
      isAuthenticated: !!user
    }}>
      {children}
    </AuthContext.Provider>
  );
}

// 認証が必要なコンポーネントを保護するラッパー
function ProtectedRoute({ children }) {
  const { user, loading } = useAuth();

  if (loading) {
    return <div>読み込み中...</div>;
  }

  if (!user) {
    return <LoginForm />;
  }

  return children;
}

// ログインフォーム
function LoginForm() {
  const { login } = useAuth();
  const [formData, setFormData] = useState({
    email: '',
    password: ''
  });
  const [error, setError] = useState('');

  const handleSubmit = async (e) => {
    e.preventDefault();
    const result = await login(formData.email, formData.password);
    
    if (!result.success) {
      setError(result.error);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <h2>ログイン</h2>
      {error && <p style={{ color: 'red' }}>{error}</p>}
      <input
        type="email"
        value={formData.email}
        onChange={(e) => setFormData({ ...formData, email: e.target.value })}
        placeholder="メールアドレス"
        required
      />
      <input
        type="password"
        value={formData.password}
        onChange={(e) => setFormData({ ...formData, password: e.target.value })}
        placeholder="パスワード"
        required
      />
      <button type="submit">ログイン</button>
    </form>
  );
}

// ダッシュボード
function Dashboard() {
  const { user, logout } = useAuth();

  return (
    <div>
      <h1>ダッシュボード</h1>
      <p>ようこそ、{user.name}さん</p>
      <button onClick={logout}>ログアウト</button>
    </div>
  );
}

この認証システムでは、ログイン状態をアプリ全体で管理できます。 ページを更新しても、ログイン状態が保持されるのも便利ですね。

複数のContextを組み合わせる

実際のアプリでは、複数のContextを同時に使うことがよくあります。

Contextの階層化

複数のProviderを効率よく管理する方法を見てみましょう。

import React, { createContext, useContext } from 'react';

// 複数のProvider をまとめるコンポーネント
function AppProviders({ children }) {
  return (
    <AuthProvider>
      <ThemeProvider>
        <NotificationProvider>
          <UserPreferencesProvider>
            {children}
          </UserPreferencesProvider>
        </NotificationProvider>
      </ThemeProvider>
    </AuthProvider>
  );
}

// 通知システム
const NotificationContext = createContext();

function useNotification() {
  const context = useContext(NotificationContext);
  if (!context) {
    throw new Error('useNotification must be used within a NotificationProvider');
  }
  return context;
}

function NotificationProvider({ children }) {
  const [notifications, setNotifications] = useState([]);

  const addNotification = (message, type = 'info') => {
    const id = Date.now();
    const notification = { id, message, type };
    
    setNotifications(prev => [...prev, notification]);
    
    // 5秒後に自動削除
    setTimeout(() => {
      removeNotification(id);
    }, 5000);
  };

  const removeNotification = (id) => {
    setNotifications(prev => prev.filter(n => n.id !== id));
  };

  return (
    <NotificationContext.Provider value={{
      notifications,
      addNotification,
      removeNotification
    }}>
      {children}
    </NotificationContext.Provider>
  );
}

// ユーザー設定
const UserPreferencesContext = createContext();

function useUserPreferences() {
  const context = useContext(UserPreferencesContext);
  if (!context) {
    throw new Error('useUserPreferences must be used within a UserPreferencesProvider');
  }
  return context;
}

function UserPreferencesProvider({ children }) {
  const [preferences, setPreferences] = useState({
    language: 'ja',
    timezone: 'Asia/Tokyo',
    notifications: {
      email: true,
      push: false,
      sms: false
    }
  });

  const updatePreferences = (updates) => {
    setPreferences(prev => ({ ...prev, ...updates }));
  };

  return (
    <UserPreferencesContext.Provider value={{
      preferences,
      updatePreferences
    }}>
      {children}
    </UserPreferencesContext.Provider>
  );
}

// 複数のContext を使用するコンポーネント
function UserDashboard() {
  const { user } = useAuth();
  const { theme } = useTheme();
  const { addNotification } = useNotification();
  const { preferences } = useUserPreferences();

  const handleSaveSettings = () => {
    // 設定保存処理
    addNotification('設定が保存されました', 'success');
  };

  return (
    <div>
      <h1>ユーザーダッシュボード</h1>
      <p>ユーザー: {user.name}</p>
      <p>テーマ: {theme}</p>
      <p>言語: {preferences.language}</p>
      <button onClick={handleSaveSettings}>設定を保存</button>
    </div>
  );
}

// 通知表示コンポーネント
function NotificationList() {
  const { notifications, removeNotification } = useNotification();

  return (
    <div className="notification-container">
      {notifications.map(notification => (
        <div 
          key={notification.id} 
          className={`notification ${notification.type}`}
        >
          <span>{notification.message}</span>
          <button onClick={() => removeNotification(notification.id)}>
            ×
          </button>
        </div>
      ))}
    </div>
  );
}

AppProvidersで複数のProviderをまとめることで、管理がとても楽になります。 各Contextは独立しているので、機能ごとに分けて開発できるのも便利ですね。

パフォーマンスを最適化する

useContextを使う時に気をつけたいパフォーマンスの話をしましょう。

不要な再レンダリングを防ぐ

Contextの値が変わると、それを使うすべてのコンポーネントが再レンダリングされます。 これを最適化する方法を見てみましょう。

import React, { createContext, useContext, useState, useMemo, memo } from 'react';

const UserContext = createContext();

function UserProvider({ children }) {
  const [user, setUser] = useState({
    name: '田中太郎',
    email: 'tanaka@example.com',
    settings: {
      theme: 'light',
      notifications: true
    }
  });

  // Context の値をメモ化
  const contextValue = useMemo(() => ({
    user,
    setUser,
    updateUser: (updates) => {
      setUser(prev => ({ ...prev, ...updates }));
    },
    updateSettings: (settingUpdates) => {
      setUser(prev => ({
        ...prev,
        settings: { ...prev.settings, ...settingUpdates }
      }));
    }
  }), [user]);

  return (
    <UserContext.Provider value={contextValue}>
      {children}
    </UserContext.Provider>
  );
}

// メモ化されたコンポーネント
const UserProfile = memo(function UserProfile() {
  const { user } = useContext(UserContext);
  console.log('UserProfile rendered'); // デバッグ用
  
  return (
    <div>
      <h2>ユーザープロフィール</h2>
      <p>名前: {user.name}</p>
      <p>メールアドレス: {user.email}</p>
    </div>
  );
});

const UserSettings = memo(function UserSettings() {
  const { user, updateSettings } = useContext(UserContext);
  console.log('UserSettings rendered'); // デバッグ用
  
  return (
    <div>
      <h2>設定</h2>
      <label>
        <input
          type="checkbox"
          checked={user.settings.notifications}
          onChange={(e) => updateSettings({ notifications: e.target.checked })}
        />
        通知を受け取る
      </label>
    </div>
  );
});

useMemoでContextの値をメモ化することで、不要な再レンダリングを防げます。 memoでコンポーネントをラップするのも効果的ですね。

Contextを分割する

関連する状態ごとにContextを分けると、さらに最適化できます。

// ユーザー情報のContext
const UserInfoContext = createContext();

function UserInfoProvider({ children }) {
  const [userInfo, setUserInfo] = useState({
    name: '田中太郎',
    email: 'tanaka@example.com'
  });

  const value = useMemo(() => ({
    userInfo,
    setUserInfo
  }), [userInfo]);

  return (
    <UserInfoContext.Provider value={value}>
      {children}
    </UserInfoContext.Provider>
  );
}

// ユーザー設定のContext
const UserSettingsContext = createContext();

function UserSettingsProvider({ children }) {
  const [userSettings, setUserSettings] = useState({
    theme: 'light',
    notifications: true,
    language: 'ja'
  });

  const value = useMemo(() => ({
    userSettings,
    setUserSettings,
    updateSettings: (updates) => {
      setUserSettings(prev => ({ ...prev, ...updates }));
    }
  }), [userSettings]);

  return (
    <UserSettingsContext.Provider value={value}>
      {children}
    </UserSettingsContext.Provider>
  );
}

// 分割されたContext を使用
function UserProfile() {
  const { userInfo } = useContext(UserInfoContext);
  // userSettings が変更されても、このコンポーネントは再レンダリングされない
  
  return (
    <div>
      <h2>ユーザープロフィール</h2>
      <p>名前: {userInfo.name}</p>
      <p>メールアドレス: {userInfo.email}</p>
    </div>
  );
}

function UserSettings() {
  const { userSettings, updateSettings } = useContext(UserSettingsContext);
  // userInfo が変更されても、このコンポーネントは再レンダリングされない
  
  return (
    <div>
      <h2>設定</h2>
      <label>
        <input
          type="checkbox"
          checked={userSettings.notifications}
          onChange={(e) => updateSettings({ notifications: e.target.checked })}
        />
        通知を受け取る
      </label>
    </div>
  );
}

このように、関連する状態ごとにContextを分けることで、効率的な再レンダリングが実現できます。

よくある問題と解決方法

useContextでよく遭遇する問題と、その解決方法をまとめました。

Context が undefined になる問題

問題: useContextを使おうとすると、undefinedが返される

function useUser() {
  const context = useContext(UserContext);
  
  if (context === undefined) {
    throw new Error('useUser must be used within a UserProvider');
  }
  
  return context;
}

// または、デフォルト値を設定
const UserContext = createContext({
  user: null,
  setUser: () => {},
  // デフォルト値を設定
});

この対策により、エラーの原因がすぐに分かるようになります。

過度な再レンダリング問題

問題: Contextの値が変わるたびに、すべてのコンポーネントが再レンダリングされる

// Context の値をメモ化
const contextValue = useMemo(() => ({
  user,
  setUser,
  updateUser: (updates) => setUser(prev => ({ ...prev, ...updates }))
}), [user]);

// コンポーネントをメモ化
const UserProfile = memo(function UserProfile() {
  const { user } = useUser();
  return <div>{user.name}</div>;
});

メモ化を適切に使うことで、パフォーマンスが向上します。

Contextが複雑になりすぎる問題

問題: 1つのContextにたくさんの状態を詰め込みすぎる

// 機能ごとにContext を分割
function AppProviders({ children }) {
  return (
    <UserProvider>
      <ThemeProvider>
        <NotificationProvider>
          {children}
        </NotificationProvider>
      </ThemeProvider>
    </UserProvider>
  );
}

機能ごとにContextを分けることで、管理しやすくなります。

エラーハンドリングの不備

問題: エラーが発生した時の処理が不十分

function UserProvider({ children }) {
  const [user, setUser] = useState(null);
  const [error, setError] = useState(null);

  const handleError = (error) => {
    setError(error);
    console.error('User context error:', error);
  };

  const clearError = () => {
    setError(null);
  };

  return (
    <UserContext.Provider value={{
      user,
      setUser,
      error,
      clearError,
      handleError
    }}>
      {children}
    </UserContext.Provider>
  );
}

エラーハンドリングを組み込むことで、より安定したアプリになります。

実用的なベストプラクティス

useContextを効果的に使うためのコツをまとめました。

1. カスタムフックを活用する

// ❌ 悪い例
function UserProfile() {
  const context = useContext(UserContext);
  if (!context) {
    throw new Error('Provider not found');
  }
  return <div>{context.user.name}</div>;
}

// ✅ 良い例
function useUser() {
  const context = useContext(UserContext);
  if (!context) {
    throw new Error('useUser must be used within a UserProvider');
  }
  return context;
}

function UserProfile() {
  const { user } = useUser();
  return <div>{user.name}</div>;
}

カスタムフックを使うことで、エラーハンドリングとコードの再利用性が向上します。

2. 適切な粒度でContextを分割する

// ❌ 悪い例(すべてを1つのContextに詰め込む)
const AppContext = createContext();

function AppProvider({ children }) {
  const [user, setUser] = useState(null);
  const [theme, setTheme] = useState('light');
  const [notifications, setNotifications] = useState([]);
  // ... 多くの状態
}

// ✅ 良い例(関連する状態ごとに分割)
const UserContext = createContext();
const ThemeContext = createContext();
const NotificationContext = createContext();

関連する機能ごとにContextを分けることで、管理しやすくなります。

3. TypeScriptでの型安全性

TypeScriptを使う場合は、型定義も忘れずに。

interface User {
  id: string;
  name: string;
  email: string;
}

interface UserContextType {
  user: User | null;
  setUser: (user: User | null) => void;
  updateUser: (updates: Partial<User>) => void;
}

const UserContext = createContext<UserContextType | undefined>(undefined);

function useUser(): UserContextType {
  const context = useContext(UserContext);
  if (context === undefined) {
    throw new Error('useUser must be used within a UserProvider');
  }
  return context;
}

型安全性により、開発時のエラーを防げます。

4. テストしやすい設計

// テスト用のモックProvider
function MockUserProvider({ children, mockUser }) {
  const mockValue = {
    user: mockUser,
    setUser: jest.fn(),
    updateUser: jest.fn()
  };

  return (
    <UserContext.Provider value={mockValue}>
      {children}
    </UserContext.Provider>
  );
}

// テストでの使用
test('UserProfile displays user name', () => {
  const mockUser = { name: 'テストユーザー' };
  
  render(
    <MockUserProvider mockUser={mockUser}>
      <UserProfile />
    </MockUserProvider>
  );
  
  expect(screen.getByText('テストユーザー')).toBeInTheDocument();
});

テストしやすい設計にすることで、品質の高いアプリを作れます。

まとめ

React useContextを使ったグローバル状態管理について、基本から実践まで詳しく解説しました。

useContextの主なメリット

propsの受け渡しが不要になるので、コードがスッキリします。 状態管理が一箇所に集約されるので、保守性が向上します。 再利用性の高いコンポーネントが作れます。

効果的な活用のポイント

  1. カスタムフックの作成: 使いやすいAPIを提供する
  2. 適切な粒度での分割: 関連する状態ごとにContextを分ける
  3. パフォーマンスの最適化: メモ化と適切な再レンダリング制御
  4. エラーハンドリング: 堅牢な状態管理を実装する

学習の進め方

useContextの習得は、以下の順序で進めることをおすすめします。

  1. 基本的な使い方: createContext、Provider、useContextの基本
  2. カスタムフック: より使いやすいAPIの作成
  3. 実践的な例: 認証やテーマ管理システムの実装
  4. 複数Contextの管理: 複雑な状態管理の体系化
  5. パフォーマンス最適化: 効率的な状態管理の実現

useContextは、小規模から中規模のReactアプリケーションにおいて、とても効果的な状態管理ソリューションです。 ReduxやZustandなどの外部ライブラリを検討する前に、まずuseContextを試してみることをおすすめします。

大丈夫です! 最初は複雑に感じるかもしれませんが、実際に使ってみると思っているより簡単です。

ぜひ、実際のプロジェクトでuseContextを活用して、より効率的で保守性の高いアプリケーションを作ってみてください。

関連記事