ContextとuseReducerでアプリケーションの状態管理

本記事ではReactのContextuseReducerを使い、Reduxライクにグローバルな状態管理を行います。useReducerはReact Hooksのため、v16.7以上が必要です。

例としてサイトのテーマ(よく見かけるライト/ダークテーマ風)の切り替えを行う簡易なアプリケーションを作成します。最終的に自分でテーマを作り切り替えを行えるようにします。

尚、本記事はコード内に解説コメントを記述していますが、Reduxの基礎知識がないと読み進めづらいかもしれません。React Hooksの知識も多少なり必要です。

チュートリアル

最終的なコンポーネントの階層は以下になります。

ThemeContextProvider ← stateセット
	App ←ここでdispatchしたい
    	MyForm ←ここでdispatchしたい

まずテーマとなるTheme.jsを作成します。下準備となるファイルです。

// src/components/Themes.js
import React from "react";
import reducer from "../reducers/reducer";

// React Contextの作成
const ThemeContext = React.createContext();
// Reducer関数の初期値
const initialState = {
  color: {
    name: "Light",
    text: "#000000",
    back: "#FFFFFF"
  }
};

// コンテキストプロバイダーコンポーネント
function ThemeContextProvider(props) {
  // useReducerでreducer関数と初期値をセット
  const [state, dispatch] = React.useReducer(reducer, initialState);
  const value = { state, dispatch };

  return (
    // コンテキストプロバイダーとしてuseReducerのstateとdispatchをコンテキストに設定
    <ThemeContext.Provider value={value}>
      {props.children}
    </ThemeContext.Provider>
  );
}

// コンテキストコンシュマーの作成(今回未使用)
const ThemeContextConsumer = ThemeContext.Consumer;

export {
  initialState,
  ThemeContext,
  ThemeContextProvider,
  ThemeContextConsumer
};

重要なのはuseReducer HookのstatedispatchをContextプロバイダーの値として渡している点です。これにより、ThemeContextProviderコンポーネントより下のコンポーネントは、Contextを通してこの2つにアクセスできるようになります。

実際に利用しているのがindex.jsです。

// src/index.js
import React from "react";
import ReactDOM from "react-dom";
import { ThemeContextProvider } from "./components/Theme";
import App from "./components/App";
import "./style.css";

const rootElement = document.getElementById("root");
ReactDOM.render(
  <ThemeContextProvider>
    <App />
  </ThemeContextProvider>,
  rootElement
);

Appコンポーネントをラップしているので、App以下では先程セットしたstatedispatchが利用できるようになります。

次にreducer関数を作成します。dispatchされたときに呼ばれる関数です。

// reducers/reducer.js
import { initialState } from "../components/Theme";

// reducer関数
const reducer = (state, action) => {
  switch (action.type) {
    case "change-color":
      return { ...state, ...action.payload };
    case "reset-color":
      return initialState;
    default:
      return state;
  }
};

export default reducer;

Reduxのreducer関数と意味的に違いはありません。action.typeに応じて、処理を分岐させて新しい状態を返すのがreducerの役割です。今回はテーマの変更とリセットの2つのアクションタイプがあります。

メインコンポーネントのApp.jsを作成します。

import React from "react";
import { ThemeContext } from "./Theme";
import MyForm from "./MyForm";

function App(props) {
  // useContextでThemeContextのstateとdispatchを使用する(コンテキスト値)
  const { state, dispatch } = React.useContext(ThemeContext);

  // state.colorの変更に併せてスタイルを変更する
  React.useEffect(
    () => {
      document.body.style.backgroundColor = state.color.back;
      document.body.style.color = state.color.text;
    },
    [state.color]
  );

  // "reset-color"をdispatchしてテーマリセットを行うハンドラ関数
  const resetColor = () =>
    dispatch({
      type: "reset-color"
    });

  // "chnage-color"をdispatchしてテーマ変更を行うハンドラ関数
  // payloadには変更する値が入る
  const setColor = color => () =>
    dispatch({
      type: "change-color",
      payload: {
        color: { name: color.name, text: color.text, back: color.back }
      }
    });

  return (
    <>
      <h1>テーマカスタマイズ</h1>
      <p>現在の情報</p>
      <ul>
        <li>テーマ名: {state.color.name}</li>
        <li>文字色: {state.color.text}</li>
        <li>背景色: {state.color.back}</li>
      </ul>
      <h2>新しいテーマを作成</h2>
      <p>16進数と色名(例:red)での指定が可能です。</p>
      {/* テーマ変更のボタン */}
      <button onClick={setColor({
      		name: "Dark", text: "#ffffff", back: "#000000"
      })}>
          変更
      </button>
      <button onClick={resetColor}>初期テーマにリセット</button>
    </>
  );
}

export default App;

ボタンをクリックすることでテーマの変更(Light⇔Dark)の変更ができるようになります。

ライトテーマとダークテーマの切り替え

重要なのはuseContextを使い、ThemeContextからstatedispatchを参照させている点です。

const { state, dispatch } = React.useContext(ThemeContext);

これにより、ThemeContextProviderのプロバイダーのvalueとしてセットしたreducer関数と状態値が利用できるわけです。変更・リセットボタンをクリックするとdispatchがコールされ、reducer関数に処理が移り新しい状態を返します。

これで2つのテーマの切り替えはできるようになりました。

オリジナルテーマを作成できるようにする

上記でContextとuseReducerを使った解説は終わりですが、最後に自分で自由にテーマを作り変えることができるようにしてみます。

先のbuttonコンポーネント2つを削除し、新しいコンポーネントを挿入します。

import MyForm from "./MyForm";

<MyForm theme={state.color} setColor={setColor} resetColor={resetColor} /> 

フォーム用のコンポーネントを作成します。


// src/components/MyForm.js
import React from "react";

function MyForm({ theme, setColor, resetColor }) {
  // 個々の値をuseStateで管理する
  const [name, setName] = React.useState(theme.name);
  const [text, setText] = React.useState(theme.text);
  const [back, setBack] = React.useState(theme.back);

  return (
    <form onSubmit={e => e.preventDefault()}>
      <label htmlFor="name">テーマ名</label>
      <input
        type="text"
        value={name}
        name="name"
        onChange={e => setName(e.target.value)}
      />
      <label htmlFor="text">文字色</label>
      <input
        type="text"
        value={text}
        name="text"
        maxLength="7"
        onChange={e => setText(e.target.value)}
      />
      <label htmlFor="back">背景色</label>
      <input
        type="text"
        value={back}
        name="back"
        maxLength="7"
        onChange={e => setBack(e.target.value)}
      />
      <p>
        <button onClick={setColor({ name, text, back })}>変更</button>
        <button onClick={resetColor}>初期テーマにリセット</button>
      </p>
    </form>
  );
}

export default MyForm;

useStateで入力値を保持させているだけの、単なるフォームです。実行すると、以下のように好みのテーマを作成して切り替えができるようになります。

自分のテーマを作成

最終的なコードはSandboxでライブプレビューが可能です。

おわりに

Reduxの代替になるわけではないですが、ContextとuseReducerを使うとReduxよりも簡単にグローバルな状態管理ができそうです。

ReactNというサードHooksもあり、これを使うとContext不要で同様のことが可能になります。現状、Hooksすらリリースされていないため仕様変更はあるかもしれませんが、チェックしておくといいかもしれません。