React Context Provider 使い方完全ガイド|初心者向け解説とよくあるエラー対策

React / Next.js

React Context Provider 使い方完全ガイド|初心者向け解説とよくあるエラー対策

はじめに

React開発をしていると、「複数のコンポーネント間でデータを共有したい」という場面に出くわします。特に深い階層のコンポーネントにデータを渡す際、従来のPropsを使った方法(いわゆる「Prop Drilling」)は非常に面倒です。

このような問題を解決するために、Reactが提供する強力な機能がContext APIです。本記事では、Context APIの中でも重要な要素であるContext Providerの使い方を、初心者向けに詳しく解説します。

React Context Providerとは

Context Providerは、Reactアプリケーション内でグローバルな状態管理を実現するための仕組みです。従来のProps経由でデータを親から子へ渡していく方法とは異なり、Context Providerを使うことで、コンポーネントツリーの任意の深さにあるコンポーネントに直接データを届けることができます。

Prop Drillingの問題点

Context Providerを理解する前に、なぜこれが必要なのかを知ることが重要です。まず、従来のPropsを使ったデータ受け渡しの問題点を見てみましょう。

// ❌ Prop Drilling の例
function App() {
  const user = { name: 'Taro', id: 1 };
  return <Parent user={user} />;
}

function Parent({ user }) {
  return <Child user={user} />;
}

function Child({ user }) {
  return <GrandChild user={user} />;
}

function GrandChild({ user }) {
  return <div>{user.name}</div>;
}

上記の例では、中間のコンポーネント(ParentとChild)は実際にはuserデータを使っていません。それでも、深くネストされたコンポーネントにデータを渡すためだけにPropsを経由させる必要があります。これが「Prop Drilling」と呼ばれる問題です。

Context Providerの原因と仕組み

Context Providerが必要な理由

Prop Drillingの問題を解決するために、ReactはContext APIを提供しています。Context Providerを使うことで:

  • 中間コンポーネントを経由せずにデータを受け渡せる
  • コンポーネント階層が深くなっても管理が簡単
  • グローバルな状態を効率的に管理できる
  • コードの可読性が向上する

Context Providerの仕組み

Context Providerの動作フローは以下の通りです:

  1. React.createContext()でContextオブジェクトを作成
  2. Context.Providerでコンポーネントをラップ
  3. Providerのvalueプロップでデータを提供
  4. 子孫コンポーネント内でuseContextフックを使ってデータを取得

Context Providerの使い方|解決手順

ステップ1:Contextの作成

まず、React.createContext()を使ってContextオブジェクトを作成します。

// UserContext.js
import { createContext } from 'react';

const UserContext = createContext();

export default UserContext;

createContext()は、初期値を引数として受け取ることもできます。上記の例では初期値を設定していないため、デフォルト値はundefinedです。

ステップ2:Providerコンポーネントの作成

次に、Contextの値を提供する親コンポーネントを作成します。通常は、アプリケーションのルートに近い場所にProviderを配置します。

// App.js
import { useState } from 'react';
import UserContext from './UserContext';
import Parent from './Parent';

function App() {
  const [user, setUser] = useState({ name: 'Taro', id: 1 });

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

export default App;

Providervalueプロップに、提供したいデータオブジェクトを渡します。ここでは、usersetUserの両方を含めることで、子孫コンポーネントで状態の読み取りと更新が可能になります。

ステップ3:useContextフックでデータを取得

子孫コンポーネント内でuseContextフックを使ってデータを取得します。

// GrandChild.js
import { useContext } from 'react';
import UserContext from './UserContext';

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

  const updateUser = () => {
    setUser({ ...user, name: 'Hanako' });
  };

  return (
    <div>
      <p>ユーザー名: {user.name}</p>
      <button onClick={updateUser}>名前を更新</button>
    </div>
  );
}

export default GrandChild;

ここで重要なのは、useContextを使って取得できるのは、Provider配下にあるコンポーネントからのみという点です。Provider外のコンポーネントからuseContextを呼び出すと、undefinedが返されます。

ステップ4:完全な実装例

ここまでのステップを統合した完全な実装例を示します。

// UserContext.js
import { createContext } from 'react';

const UserContext = createContext();
export default UserContext;

// App.js
import { useState } from 'react';
import UserContext from './UserContext';
import Parent from './Parent';

function App() {
  const [user, setUser] = useState({ name: 'Taro', age: 30 });

  return (
    <UserContext.Provider value={{ user, setUser }}>
      <h1>Context Provider の例</h1>
      <Parent />
    </UserContext.Provider>
  );
}

export default App;

// Parent.js
import Child from './Child';

function Parent() {
  // Parentではpropsを受け取らない
  return (
    <div>
      <h2>Parent コンポーネント</h2>
      <Child />
    </div>
  );
}

export default Parent;

// Child.js
import GrandChild from './GrandChild';

function Child() {
  // ChildもContextを使わない
  return (
    <div>
      <h3>Child コンポーネント</h3>
      <GrandChild />
    </div>
  );
}

export default Child;

// GrandChild.js
import { useContext } from 'react';
import UserContext from './UserContext';

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

  const incrementAge = () => {
    setUser({ ...user, age: user.age + 1 });
  };

  return (
    <div>
      <h4>GrandChild コンポーネント</h4>
      <p>名前: {user.name}</p>
      <p>年齢: {user.age}</p>
      <button onClick={incrementAge}>年齢を増やす</button>
    </div>
  );
}

export default GrandChild;

この例では、App.jsのProviderでuserデータを提供し、GrandChild.jsで直接そのデータにアクセスしています。中間のParentとChildコンポーネントはContextについて何も知りません。

よくある間違いと対策

間違い1:Provider外でuseContextを使用

最も一般的なエラーは、Providerの配下にないコンポーネント内でuseContextを呼び出すことです。

// ❌ 間違い:ProviderがないのにuseContextを使用
function OutsideComponent() {
  const { user } = useContext(UserContext); // エラー: undefined
  return <div>{user.name}</div>; // クラッシュする
}

// ✅ 正しい:Providerの中にある
function App() {
  return (
    <UserContext.Provider value={{ user: { name: 'Taro' } }}>
      <InsideComponent />
    </UserContext.Provider>
  );
}

function InsideComponent() {
  const { user } = useContext(UserContext);
  return <div>{user.name}</div>;
}

対策として、Contextを使うコンポーネントは必ずProviderの子孫になるように配置してください。

間違い2:valueプロップでオブジェクトを直接作成

valueプロップで毎回新しいオブジェクトを作成すると、不要な再レンダリングが発生します。

// ❌ 間違い:毎回新しいオブジェクトが作成される
function App() {
  const user = { name: 'Taro' };
  return (
    <UserContext.Provider value={{ user, name: 'app' }}>
      {/* このvalue={...}は毎レンダリング時に新しいオブジェクトになる */}
      <Child />
    </UserContext.Provider>
  );
}

// ✅ 正しい:valueを状態で管理
function App() {
  const [user, setUser] = useState({ name: 'Taro' });
  const value = useMemo(() => ({ user, setUser }), [user]);
  
  return (
    <UserContext.Provider value={value}>
      <Child />
    </UserContext.Provider>
  );
}

useMemoを使ってvalueオブジェクトをメモ化することで、不要な再レンダリングを防げます。

間違い3:複数のContextを効率的に管理していない

複数のContextが必要な場合、Providerをネストさせすぎるとコードが複雑になります。

// ❌ 間違い:Providerが深くネストしている
function App() {
  return (
    <UserContext.Provider value={userValue}>
      <ThemeContext.Provider value={themeValue}>
        <NotificationContext.Provider value={notificationValue}>
          <MainApp />
        </NotificationContext.Provider>
      </ThemeContext.Provider>
    </UserContext.Provider>
  );
}

// ✅ 正しい:カスタムProviderコンポーネントでラップ
function AppProviders({ children }) {
  return (
    <UserContext.Provider value={userValue}>
      <ThemeContext.Provider value={themeValue}>
        <NotificationContext.Provider value={notificationValue}>
          {children}
        </NotificationContext.Provider>
      </ThemeContext.Provider>
    </UserContext.Provider>
  );
}

function App() {
  return (
    <AppProviders>
      <MainApp />
    </AppProviders>
  );
}

間違い4:useContextの値がundefinedになる

Contextが適切に初期化されていない場合、useContextで取得した値がundefinedになることがあります。

// ❌ 間違い:初期値がないため、Providerが見つからないとundefined
const UserContext = createContext(); // 初期値なし

function Child() {
  const context = useContext(UserContext);
  if (!context) {
    return <div>エラー:UserContextが見つかりません</div>
  }
  return <div>{context.user.name}</div>;
}

// ✅ 正しい:初期値を設定またはProviderでラップ
const UserContext = createContext({ user: null });

function Child() {
  const { user } = useContext(UserContext);
  if (!user) {
    return <div>ユーザーがログインしていません</div>
  }
  return <div>{user.name}</div>
}

間違い5:Context内の値の更新が反映されない

setStateで状態を更新した場合、Contextの値も自動的に更新される必要があります。

// ❌ 間違い:valueが変わらない
function App() {
  const [user, setUser] = useState({ name: 'Taro' });
  const staticValue = { user }; // 毎回新しいオブジェクト
  
  return (
    <UserContext.Provider value={staticValue}>
      <Child />
    </UserContext.Provider>
  );
}

// ✅ 正しい:valueを適切に設定
function App() {
  const [user, setUser] = useState({ name: 'Taro' });
  const value = useMemo(() => ({ user, setUser }), [user]);
  
  return (
    <UserContext.Provider value={value}>
      <Child />
    </UserContext.Provider>
  );
}

Context Provider のベストプラクティス

カスタムフックの作成

useContextを毎回手動で呼び出すのは面倒です。カスタムフックを作成することで、コードを簡潔にできます。

// useUserContext.js
import { useContext } from 'react';
import UserContext from './UserContext';

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

export default useUserContext;

// GrandChild.js
import useUserContext from './useUserContext';

function GrandChild() {
  const { user, setUser } = useUserContext();
  
  return (
    <div>
      <p>{user.name}</p>
    </div>
  );
}

export default GrandChild;

Providerコンポーネントの分離

Providerロジックを別ファイルに分離することで、管理が容易になります。

// UserProvider.js
import { useState, useMemo } from 'react';
import UserContext from './UserContext';

function UserProvider({ children }) {
  const [user, setUser] = useState({ name: 'Taro', age: 30 });
  const value = useMemo(() => ({ user, setUser }), [user]);

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

export default UserProvider;

// App.js
import UserProvider from './UserProvider';
import Child from './Child';

function App() {
  return (
    <UserProvider>
      <Child />
    </UserProvider>
  );
}

export default App;

パフォーマンス最適化のコツ

Context APIを使う際、パフォーマンスに注意が必要です。以下のコツを参考にしてください。

1. 複数のContextに分割する

大きなContextは、複数の小さなContextに分割するとパフォーマンスが向上します。

// ❌ すべてを1つのContextに
const AppContext = createContext();

function App() {
  const [user, setUser] = useState({});
  const [theme, setTheme] = useState({});
  const [notifications, setNotifications] = useState([]);
  
  return (
    <AppContext.Provider value={{ user, setUser, theme, setTheme, notifications, setNotifications }}>
      <Child />
    </AppContext.Provider>
  );
}

// ✅ Contextを分割
const UserContext = createContext();
const ThemeContext = createContext();
const NotificationContext = createContext();

function App() {
  const [user, setUser] = useState({});
  const [theme, setTheme] = useState({});
  const [notifications, setNotifications] = useState([]);
  
  return (
    <UserContext.Provider value={{ user, setUser }}>
      <ThemeContext.Provider value={{ theme, setTheme }}>
        <NotificationContext.Provider value={{ notifications, setNotifications }}>
          <Child />
        </NotificationContext.Provider>
      </ThemeContext.Provider>
    </UserContext.Provider>
  );
}

2. useCallbackで関数をメモ化

import { useState, useMemo, useCallback } from 'react';
import UserContext from './UserContext';

function UserProvider({ children }) {
  const [user, setUser] = useState({ name: 'Taro' });
  
  const updateUser = useCallback((newName) => {
    setUser(prev => ({ ...prev, name: newName }));
  }, []);
  
  const value = useMemo(
    () => ({ user, updateUser }),
    [user, updateUser]
  );

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

export default UserProvider;

実践的な例:認証システム

Context Providerの実践的な使用例として、簡単な認証システムを実装してみます。

// AuthContext.js
import { createContext } from 'react';

const AuthContext = createContext();
export default AuthContext;

// AuthProvider.js
import { useState, useMemo } from 'react';
import AuthContext from './AuthContext';

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

  const login = async (email, password) => {
    setLoading(true);
    // APIコールをシミュレート
    setTimeout(() => {
      setIsAuthenticated(true);
      setUser({ email, id: 1 });
      setLoading(false);
    }, 1000);
  };

  const logout = () => {
    setIsAuthenticated(false);
    setUser(null);
  };

  const value = useMemo(
    () => ({ isAuthenticated, user, loading, login, logout }),
    [isAuthenticated, user, loading]
  );

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

export default AuthProvider;

// LoginForm.js
import { useState } from 'react';
import { useAuthContext } from './useAuthContext';

function LoginForm() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const { login, loading } = useAuthContext();

  const handleSubmit = (e) => {
    e.preventDefault();
    login(email, password);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="メールアドレス"
      />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        placeholder="パスワード"
      />
      <button type="submit" disabled={loading}>
        {loading ? 'ログイン中...' : 'ログイン'}
      </button>
    </form>
  );
}

export default LoginForm;

// useAuthContext.js
import { useContext } from 'react';
import AuthContext from './AuthContext';

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

export default useAuthContext;

// App.js
import AuthProvider from './AuthProvider';
import LoginForm from './LoginForm';

function App() {
  return (
    <AuthProvider>
      <h1>認証システムの例</h1>
      <LoginForm />
    </AuthProvider>
  );
}

export default App;

まとめ

React Context Providerは、Reactアプリケーションでグローバルな状態管理を実現するための強力なツールです。本記事で学んだポイントを振り返りましょう。

重要なポイント

  • Context Providerの目的:Prop Drillingを避け、深くネストされたコンポーネント間でのデータ共有を効率化する
  • 実装の流れ:Contextの作成 → Providerでラップ → useContextで取得
  • よくある間違い:Provider外でのuseContext使用、valueの毎回新規作成、複数Contextの管理不足
  • ベストプラクティス:カスタムフックの作成、Providerコンポーネントの分離、パフォーマンスの最適化
  • パフォーマンス:useMemoとuseCallbackを活用し、不要な再レンダリングを防ぐ

次のステップ

Context APIを習得したら、より高度な状態管理パターンに挑戦してみてください。

  • useReducerとContextの組み合わせ
  • 複数のContextの効率的な管理
  • Redux や Zustand などの外部状態管理ライブラリの検討
  • パフォーマンス計測と最適化

Context Providerは、小〜中規模なReactアプリケーションの状態管理に最適です。適切に使用することで、コードの可読性と保守性が大幅に向上します。本記事の例を参考に、実際のプロジェクトで活用してみてください。

タイトルとURLをコピーしました