Reactでグローバルな状態を管理する【Context/useContext】

JavaScript

グローバルな状態管理の方法は複数ありますが、Reactに標準で実装されてるReactHooksのContextuseContextの使い方をまとめたのでメモ。

グローバルな状態管理とは

コンポーネント間で値を渡すにはPropsを使いますが、コンポーネントの階層が深い場合、バケツリレーのように親から子へと順々に値を渡していきます。

Propsで値を渡す(バケツリレー)

階層が深くなりすぎると管理が煩雑になるため、どのコンポーネントからでもアクセスできるグローバルな値を管理したい時があります。

特にStateを使った状態管理の値を、どこからでもアクセスして更新したい!ってことは多いです。
今回、Reactに標準で実装されてるReactHooksのContextuseContextを使います。

Contextで値を渡す

サンプル:StateをPropsで渡した場合(バケツリレー)

まずは、普通にPropsで状態管理する単純なサンプルを見てみます。以下は子コンポーネントにあるボタンをクリックしたら、Stateの状態(ON/OFF)が更新されます。まずは動作をご確認ください!

構造は次のようになっています。
useStateで状態管理をしているのはホームコンポーネントです。
この値を子コンポーネントのボタンで更新します。

ホームコンポーネントで管理しているStateを子コンポーネントに渡す必要がありますが、子コンポーネントに直接Stateを渡す方法はありません。Propsで親コンポーネントを介して渡すことになります。

コード全体は次のようになります。※関係のある箇所に色をつけました。

import React, { FC, useState } from "react";
import "./styles.css";

type switchFlagType = {
  switchFlag: boolean;
  setSwitchFlag: React.Dispatch<React.SetStateAction<boolean>>;
};

// 子コンポーネント
const Child: FC<switchFlagType> = (props) => {
  // 分割代入でPropsの値を受け取る
  const { switchFlag, setSwitchFlag } = props;

  return (
    <div className="child">
      <h1>子コンポーネント</h1>
      <button onClick={() => setSwitchFlag(!switchFlag)}>
        {switchFlag ? "OFFにする" : "ONにする"}
      </button>
    </div>
  );
};

// 親コンポーネント
const Parent: FC<switchFlagType> = (props) => {
  // 分割代入でPropsの値を受け取る
  const { switchFlag, setSwitchFlag } = props;
  return (
    <div className="parent">
      <h1>親コンポーネント</h1>

      {/* 子コンポーネントにStateを渡す */}
      <Child switchFlag={switchFlag} setSwitchFlag={setSwitchFlag} />
    </div>
  );
};

// Homeコンポーネント
const Home: FC = () => {
  // スイッチのState管理
  const [switchFlag, setSwitchFlag] = useState(false);
  return (
    <div className="home">
      <h1>ホームコンポーネント</h1>

      {/* stateの状態を表示 */}
      <p className="switchTxt">{switchFlag ? "ON" : "OFF"}</p>

      {/* 親コンポーネントにStateを渡す */}
      <Parent switchFlag={switchFlag} setSwitchFlag={setSwitchFlag} />
    </div>
  );
};

// APPコンポーネント
export const App: FC = () => {
  return (
    <div className="App">
      <Home />
    </div>
  );
};
export default App;

ホームコンポーネントから子コンポーネントにStateが渡るまで(バケツリレー)

親から子へ順々に値を渡していくので、よくバケツリレーと表現されてます。

Homeコンポーネント

まず、ホームコンポーネントで宣言したuseStateを

  const [switchFlag, setSwitchFlag] = useState(false);

次のように親コンポーネントに渡しています。

<Parent switchFlag={switchFlag} setSwitchFlag={setSwitchFlag} />

Parentコンポーネント

親コンポーネントで受け取ったPropsの値を、次のように分割代入で取り出します。

const { switchFlag, setSwitchFlag } = props;

取り出したStateを子コンポーネントに渡します。

<Child switchFlag={switchFlag} setSwitchFlag={setSwitchFlag} />

Childコンポーネント

子コンポーネントで受け取ったPropsの値を、親コンポーネントと同じように分割代入で取り出します。

const { switchFlag, setSwitchFlag } = props;

これでようやく子コンポーネントでStateが使えます。※ボタンのイベントに使用

<button onClick={() => setSwitchFlag(!switchFlag)}>

これくらい見渡しが良いコードだと問題ないのですが、コンポーネントが別ファイルにあったり、影響が広範囲に渡ると、状態管理の把握が難しくなります。
そこで使えるのがContextuseContextです。バケツリレーではなく、グローバルな値に直接アクセスできます。

サンプル:StateをContextとuseContextで管理する場合

先程のサンプルをContextuseContextで再現しました。
動きは全く同じです。

コード全体は次のようになります。※関係のある箇所に色をつけました。
先程のPropsでStateを渡す例と比べて、親コンポーネントにPropsの設定がないことが確認できます。

import React, { FC, useState, createContext, useContext } from "react";
import "./styles.css";

type switchFlagType = {
  switchFlag: boolean;
  setSwitchFlag: React.Dispatch<React.SetStateAction<boolean>>;
};

// Contextを作成する
const SwitchContext = createContext<switchFlagType>({} as switchFlagType);

// Context.ProviderでChildrenを囲む
const SwitchProvider: FC = (props) => {
  // スイッチのState管理
  const [switchFlag, setSwitchFlag] = useState(false);
  // PropsからChildrenを取得
  const { children } = props;
  // Providerに渡すvalue値(State)を設定
  const value: switchFlagType = {
    switchFlag,
    setSwitchFlag
  };
  // valueを設定したContext.Providerを返却する
  return (
    <SwitchContext.Provider value={value}>{children}</SwitchContext.Provider>
  );
};

// 子コンポーネント
const Child: FC = () => {
  // Contextのvalue値を参照
  const { switchFlag, setSwitchFlag } = useContext(SwitchContext);
  return (
    <div className="child">
      <h1>子コンポーネント</h1>
      <button onClick={() => setSwitchFlag(!switchFlag)}>
        {switchFlag ? "OFFにする" : "ONにする"}
      </button>
    </div>
  );
};

// 親コンポーネント
const Parent: FC = () => {
  return (
    <div className="parent">
      <h1>親コンポーネント</h1>
      <Child />
    </div>
  );
};

// Homeコンポーネント
const Home: FC = () => {
  // Contextのvalue値を参照
  const { switchFlag } = useContext(SwitchContext);
  return (
    <div className="home">
      <h1>ホームコンポーネント</h1>

      {/* stateの状態を表示 */}
      <p className="switchTxt">{switchFlag ? "ON" : "OFF"}</p>
      <Parent />
    </div>
  );
};

// APPコンポーネント
export const App: FC = () => {
  return (
    <SwitchProvider>
      <div className="App">
        <Home />
      </div>
    </SwitchProvider>
  );
};
export default App;

Stateの値はPropsではなく、Contextで一元管理します。設定までの手順を見ていきます!

createContext関数でContextオブジェクトを作る

Contextは、Reactでグローバルな値を管理できるオブジェクトです。作成するにはcreateContext関数を使用します。
まずは、以下のようにcreateContextをインポートしておきます。

import React, { createContext } from "react";

インポートしたcreateContextでContextオブジェクトを作成します。分かりにくい概念なので、まずは構文をそのまま覚えていこうと思います、、。

構文は次の通りとなります。

const 変数名 = createContext();

以下は、SwitchContextという変数名でContextオブジェクトを作成しています。※TypeScriptの記述は省略してます

const SwitchContext = createContext();

Providerコンポーネントを作成する

Contextオブジェクトは、Providerというコンポーネントを持っています。
Providerコンポーネントで囲んだ(ラップした)コンポーネントからContextで設定した値にアクセス、または更新ができる仕組みになっています。

構文は次の通りです。

<Context.Provider value={<グローバルな値>}>
    <グローバルな値にアクセスできるコンポーネント />
</Context.Provider>

これを、先程作成したSwitchContextに当てはめると次のようになります。

<SwitchContext.Provider value={<グローバルな値>}>
    <グローバルな値にアクセスできるコンポーネント />
</SwitchContext.Provider>

グローバルで管理する値をvalueに設定する

グローバルな値はvalue={}に設定します。今回、スイッチの状態をブール値で管理するuseStateをグローバルな値として管理します。※初期値はfalseです。

 // スイッチのState管理
  const [switchFlag, setSwitchFlag] = useState(false);

今回は、次のように変数valueを作成して格納します。

// Providerに渡すvalue値(State)を設定
const value = {
   switchFlag,
   setSwitchFlag
 };

Providerコンポーネントを設定する

グローバルな値にアクセスできるコンポーネントは、Contextを関数コンポーネント内に記述して、Propsの{children}として受け取ればスマートに記述できそうです。

言葉だと分かりにくいですが、コードだと次のようになります。SwitchProviderという名前の関数コンポーネントを作成します。※TypeScriptの記述は省略してます

// Context.ProviderでChildrenを囲む
const SwitchProvider = (props) => {
  // スイッチのState管理
  const [switchFlag, setSwitchFlag] = useState(false);
  // PropsからChildrenを取得
  const { children } = props;
  // Providerに渡すvalue値(State)を設定
  const value: switchFlagType = {
    switchFlag,
    setSwitchFlag
  };
  // valueを設定したContext.Providerを返却する
  return (
    <SwitchContext.Provider value={value}>{children}</SwitchContext.Provider>
  );
};

先程のStateを格納した変数vauleをProviderコンポーネントのvalue={}に設定することで、グローバルな値として使用できます。

これで<SwitchProvider>でラップしたコンポーネントから、Contextで設定したvalue値にアクセスできるようになります。

Providerコンポーネントで対象のコンポーネントをラップする

関数コンポーネント化したProviderは次のように使います。
※Providerを別ファイルにした場合はインポートする必要があります。

// APPコンポーネント
export const App = () => {
  return (
    <SwitchProvider>
      <div className="App">
        <Home />
      </div>
    </SwitchProvider>
  );
};

コンポーネント化したことで記述がスッキリしました。
<SwitchProvider>でラップされたコンポーネントからContextで設定したグローバルな値にアクセス、更新できるようになります。

useContextでグローバルな値を更新・参照する

<SwitchProvider>でラップされたコンポーネントからグローバルな値にアクセスするには、useContextを使います。

グローバル値を使いたいコンポーネントにuseContextをインポートしておきます。
Stateを更新する子コンポーネントにインポートします。

Childコンポーネント

import React, { useContext } from "react";

useContextの構文は次の通りです。

const 変数名 = useContext(<Contextオブジェクト名>);

先程、ContextオブジェクトはSwitchContextという名前にしました。構文に当てはめると次のようになります。

const context = useContext(SwitchContext);

中身を確認してみます。

// console.log(context)の出力結果
{switchFlag: false, setSwitchFlag: ƒ bound dispatchSetState()}

Contextで設定したStateの値、switchFlagsetSwitchFlagが格納されています!
これでグローバルな値が子コンポーネントで使用できることを確認できました。使いやすいように分割代入で値を取り出します。

const { switchFlag, setSwitchFlag } = useContext(SwitchContext);

そして、子コンポーネントでStateの値を更新します。

<button onClick={() => setSwitchFlag(!switchFlag)}>

Homeコンポーネント

子コンポーネントで更新したStateの値は、別のコンポーネントで参照することができます。
今回、ホームコンポーネントでStateの値を参照します。

先程と同じようにuseContextをインポートして、分割代入でグローバル値を取り出します。

// Contextのvalue値を参照
const { switchFlag } = useContext(SwitchContext);

あとは参照するだけです。今回、Stateはブール値なので、trueかfalseで表示する文字を変更しています。

// stateの状態を表示
<p className="switchTxt">{switchFlag ? "ON" : "OFF"}</p>

これで、ContextuseContextでグローバルな値を更新・参照できるようになりました!

サンプル:テーマをボタンで切り替える

勉強も兼ねて、もう少し実用性がありそうな?サンプルを作りました。
ボタン一つでテーマが切り替わります。テーマはトロピカル、レトロ、ファンタジーを用意しました!

画像が切り替わるまで、多少のタイムラグがありますが、今回は気にしないことにします。。
ポイントとなる箇所だけメモしておきます。

Contextの設定は1つのファイルにまとめる

ファイル全体の構成は次のようになります。
今回、Contextの設定は、providersディレクトリのThemeProvider.tsxに記述します。

.
├── src
│   ├── About.tsx
│   ├── App.tsx
│   ├── Contact.tsx
│   ├── Home.tsx
│   ├── components
│   │   ├── Footer.tsx
│   │   ├── Header.tsx
│   │   ├── Layout.tsx
│   │   └── ThemeChoice.tsx
│   ├── index.tsx
│   ├── providers
│   │   └── ThemeProvider.tsx
│   └── styles.css
└── tsconfig.json

./providers/ThemeProvider.tsx

こちらがContext全体のコードです。

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

type ThemeContextType = {
  themeInfo: { themeKey: string; themeName: string };
  setThemeInfo: React.Dispatch<
    React.SetStateAction<{
      themeKey: string;
      themeName: string;
    }>
  >;
};

// Contextオブジェクト作成
export const ThemeContext = createContext<ThemeContextType>(
  {} as ThemeContextType
);

// Providerをコンポーネント化する
export const ThemeProvider: FC = (props) => {
  // Propsでchildrenを取り出す
  const { children } = props;
  // テーマ(State)設定
  const [themeInfo, setThemeInfo] = useState({
    themeKey: "tropical",
    themeName: "トロピカル"
  });
  // グローバルで管理するState
  const value: ThemeContextType = {
    themeInfo,
    setThemeInfo
  };

  // valueを設定したProviderコンポーネントを返却
  return (
    <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
  );
};

useStateでテーマの名前とキーを管理します。これをグローバル値とし、どのコンポーネントからもアクセスできるようにします。※初期値はトロピカルです。

  // テーマ(State)設定
  const [themeInfo, setThemeInfo] = useState({
    themeKey: "tropical",
    themeName: "トロピカル"
  });

ポイントは、Contextオブジェクト、Providerコンポーネントをexportして、他のファイルでもインポーして使えるようにすることです。

Providerコンポーネントでアクセスできる範囲を指定する

グローバル値にアクセスできる範囲を指定します。
具体的には、Providerコンポーネント<ThemeProvider>でグリーバル値にアクセスしたいコンポーネントをラップします。
今回は、影響範囲を全てのコンポーネントにしたいので、次のように全体を囲みました。

App.tsx

import "./styles.css";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import { Home } from "./Home";
import { About } from "./About";
import { Contact } from "./Contact";
import { ThemeProvider } from "./providers/ThemeProvider";
import { ThemeContext } from "./providers/ThemeProvider";
import { useContext } from "react";

const App = () => {
  return (
    <>
      <ThemeProvider>
        <BrowserRouter>
          <Routes>
            <Route index element={<Home />} />
            <Route path="/about" element={<About />} />
            <Route path="/contact" element={<Contact />} />
          </Routes>
        </BrowserRouter>
      </ThemeProvider>
    </>
  );
};

export default App;
// useContextを全てのコンポーネントで使えるようにexportする
export const useTheme = () => useContext(ThemeContext);

useContext()をexportして共通で使えるようにする

また、useContext(<Contextオブジェクト名>)は全てのコンポーネントで使用できるようにexportしておきます。

// useContextを全てのコンポーネントで使えるようにexportする
export const useTheme = () => useContext(ThemeContext);

グローバルなStateを更新する

グローバル値の参照・更新は、先程のスイッチのサンプルと使い方は同じです。

以下は、テーマを変更するボタンをクリックしたら、グローバル値(テーマ)を更新するコンポーネントです。

./components/ThemeChoice.tsx

import { useTheme } from "../App";

export const ThemeChoice = () => {
  const { themeInfo, setThemeInfo } = useTheme();

  const onClicksetTheme = (key: string, name: string) => {
    setThemeInfo({
      themeKey: key,
      themeName: name
    });
  };

  return (
    <div className="theme-choice">
      <ul>
        <li
          className={themeInfo.themeKey === "tropical" ? "active" : ""}
          onClick={() => onClicksetTheme("tropical", "トロピカル")}
        >
          トロピカル
        </li>
        <li
          className={themeInfo.themeKey === "retro" ? "active" : ""}
          onClick={() => onClicksetTheme("retro", "レトロ")}
        >
          レトロ
        </li>
        <li
          className={themeInfo.themeKey === "fantasy" ? "active" : ""}
          onClick={() => onClicksetTheme("fantasy", "ファンタジー")}
        >
          ファンタジー
        </li>
      </ul>
    </div>
  );
};

まとめ

今回、Reactでグローバルな状態を管理する方法についてまとめました。サンプルではテーマ変更を題材にしましたが、ログイン状態の管理に使うことが多いそうです。

Contextオブジェクトは名前を変えれば複数作成できますし、Providerでアクセスできる範囲を指定できます。規模の小さいアプリの状態管理ならこれで十分な気もしますが、大規模になるとReduxという状態管理のフレームワークを使うことがあるそうです。

Reduxはなかなか理解が難しいのですが、近いうちに使い方をまとめようと思います!

コメント