【React】ContextのProviderは正しく囲まないとコンテキストが分岐する

Contextを使用する際のProviderの指定を間違うと共有したはずのコンテキストが子コンポーネント毎に分岐してしまうので注意が必要です。

サンプルコード

以下のようなContextを使って子コンポーネントにログイン情報を共有するコードを例にします。

//Contextファイル
// AuthContext.js

import React, { createContext, useState } from "react";

export const AuthContext = createContext();

export const AuthProvider = ({ children }) => {
  const [isLoggedIn, setIsLoggedIn] = useState(false);

  const toggleLogin = () => {
    setIsLoggedIn((prev) => !prev);
  };

  return (
    <AuthContext.Provider value={{ isLoggedIn, toggleLogin }}>
      {children}
    </AuthContext.Provider>
  );
};
//子コンポーネント1
//ComponentA.jsx

import React, { useContext } from "react";
import { AuthContext } from "./AuthContext";

const ComponentA = () => {
  const { isLoggedIn, toggleLogin } = useContext(AuthContext);

  return (
    <div>
      <h2>ComponentA</h2>
      <p>ログイン状態: {isLoggedIn ? "ログイン済み" : "未ログイン"}</p>
      <button onClick={toggleLogin}>
        {isLoggedIn ? "ログアウト" : "ログイン"}
      </button>
    </div>
  );
};

export default ComponentA;
//子コンポーネント2
//ComponentB.jsx

import React, { useContext } from "react";
import { AuthContext } from "./AuthContext";

const ComponentB = () => {
  const { isLoggedIn, toggleLogin } = useContext(AuthContext);

  return (
    <div>
      <h2>ComponentB</h2>
      <p>ログイン状態: {isLoggedIn ? "ログイン済み" : "未ログイン"}</p>
      <button onClick={toggleLogin}>
        {isLoggedIn ? "ログアウト" : "ログイン"}
      </button>
    </div>
  );
};

export default ComponentB;

サンプルなので子コンポーネントComponentAComponentB の中身は同じです。

コンテキストが分岐してしまう書き方

以下のような場合は子コンポーネント毎に別々のコンテキストを持つ(分岐してしまう)ことになる。

//親コンポーネント
//App.js
//誤: コンテキストが分岐する

import React from "react";
import { AuthProvider } from "./AuthContext";

const App = () => {
  return (

    <div>
      {/* 子コンポーネントが同じProviderを別々に囲う */}
      <AuthProvider>
        <ComponentA />
      </AuthProvider>
      <AuthProvider>
        <ComponentB />
      </AuthProvider>
    </div>
  );
};
  • isLoggedIn stateが子コンポーネント間で別々のstateとして扱われる
  • コンポーネント内のログインボタンでisLoggedIn stateを更新してもそれぞれの子コンポーネント内のstateしか更新されない
    • 例1) ComponentAでログインボタンクリック → ComponentAのisLoggedIn stateしか更新されない
    • 例2) ComponentBでログインボタンクリック → ComponentBのisLoggedIn stateしか更新されない

結果的にログイン状態を表示するログイン済み/未ログイン の文字の切替、ログインボタンのログイン/ログアウト の文字の切替が子コンポーネント毎にしか動作しなくなります。

コンテキストを正しく共有する書き方

以下のようにコンテクストを共有したいコンポーネントを1つのProvideで囲むことでコンテキストが正しく共有されます。

//親コンポーネント
//App.js
//正: コンテキストが正しく共有される

import React from "react";
import { AuthProvider } from "./AuthContext";

const App = () => {
  return (
    <div>
      {/* 2つの子コンポーネントを1つのProviderで囲う */}
      <AuthProvider>
        <ComponentA />
        <ComponentB />
      </AuthProvider>
    </div>
  );
};
  • 例1) ComponentAでログインボタンクリック → ComponentA、ComponentBのisLoggedIn stateが更新される
  • 例2) ComponentBでログインボタンクリック → ComponentA、ComponentBのisLoggedIn stateが更新される

ログイン状態を表すログイン済み/未ログイン の文字、ログインボタンのログイン/ログアウト の文字がどちらの子コンポーネントでも切り替わるようになります。

まとめ

  • Contextを使ってコンテキストが共有されない場合はProviderが正しく設定されているか疑う
  • 同じコンテキストを共有する複数の子コンポーネントがある場合、1つの親コンポーネント内で1つのProviderで囲むこと
  • 複数のProviderを使用する場合、それぞれが異なる状態を持つ

感想

React、便利なんだろうけど慣れるまでは修羅の道... はやく楽しく触れるようになりたい。

参照

コンテクストで深くデータを受け渡す – React

useContext – React