ContextとuseReducerでアプリケーションの状態管理
本記事ではReactのContextとuseReducerを使い、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のstate
とdispatch
を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以下では先程セットしたstate
とdispatch
が利用できるようになります。
次に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からstate
とdispatch
を参照させている点です。
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すらリリースされていないため仕様変更はあるかもしれませんが、チェックしておくといいかもしれません。