グローバルな状態管理の方法は複数ありますが、Reactに標準で実装されてるReactHooksのContextとuseContextの使い方をまとめたのでメモ。
グローバルな状態管理とは
コンポーネント間で値を渡すにはPropsを使いますが、コンポーネントの階層が深い場合、バケツリレーのように親から子へと順々に値を渡していきます。
Propsで値を渡す(バケツリレー)
階層が深くなりすぎると管理が煩雑になるため、どのコンポーネントからでもアクセスできるグローバルな値を管理したい時があります。
特にStateを使った状態管理の値を、どこからでもアクセスして更新したい!ってことは多いです。
今回、Reactに標準で実装されてるReactHooksのContextとuseContextを使います。
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)}>
これくらい見渡しが良いコードだと問題ないのですが、コンポーネントが別ファイルにあったり、影響が広範囲に渡ると、状態管理の把握が難しくなります。
そこで使えるのがContextとuseContextです。バケツリレーではなく、グローバルな値に直接アクセスできます。
サンプル:StateをContextとuseContextで管理する場合
先程のサンプルをContextとuseContextで再現しました。
動きは全く同じです。
コード全体は次のようになります。※関係のある箇所に色をつけました。
先程の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で設定した値にアクセス、または更新ができる仕組みになっています。
構文は次の通りです。
これを、先程作成した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の値、switchFlag、setSwitchFlagが格納されています!
これでグローバルな値が子コンポーネントで使用できることを確認できました。使いやすいように分割代入で値を取り出します。
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>
これで、ContextとuseContextでグローバルな値を更新・参照できるようになりました!
サンプル:テーマをボタンで切り替える
勉強も兼ねて、もう少し実用性がありそうな?サンプルを作りました。
ボタン一つでテーマが切り替わります。テーマはトロピカル、レトロ、ファンタジーを用意しました!
画像が切り替わるまで、多少のタイムラグがありますが、今回は気にしないことにします。。
ポイントとなる箇所だけメモしておきます。
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はなかなか理解が難しいのですが、近いうちに使い方をまとめようと思います!
コメント