以前書いたReact用ステート管理2020 〜Recoilを試してみました〜の時点で、主なReact用ステート管理方法には
- ReactのuseStateのみを使う
- 元祖Redux
- Redux Toolkit
- ReactのuseReducerとContextを使う
- MobX
- Recoil
- Xstate
などがありました、元祖Redux以外は現在でも使われていると思います。最近になってZustandというステート管理ライブラリーが話題になっているので調べてみました。
結論としては、とても良さそうです(アイコンの熊さんも可愛いし😊)。
ステート管理ライブラリーは2つのタイプがある
ステート管理ライブラリーは2つのタイプがあるように思えます。
① 手軽に使える
UIコンポーネントの中で気軽にステート管理が行えるライブラリー。これに該当するのは以下でしょうか、
② 体系的な管理に向く
ほとんどのステート管理ライブラリーは、こちらだと思います。手軽に使えるライブラリーで気軽にコードを作って行くと、メンテナンス性の低いコードになってしまいいます。
また、ステート管理がUIコンポーネント内にあるとコンポーネントの再利用性もさがるので、UIとステート管理・ロジックを分けるのは中規模以上のアプリでは自然な考えだと思います。
- 元祖Redux
- Redux Toolkit
- ReactのuseReducerとContextを使う
- MobX
- Xstate
Redux Toolkitは元祖Reduxの進化系ですし、useReducer + ContextもFlux(Redux)アーキテクチャの実装です。 ただし、Xstateは有限状態マシンをベースにしています→参照記事、またMobxはオブザーバー・パターンをベースにしています→参照記事。
zustandとは
まずzustandは、状態のドイツ語です。 zustandのドキュメントは以下のよう書かれています。
- Redux よりも優れている点
- シンプルで独善的ではない
- Hooksを使い倒す
- アプリをProviderコンポーネントで括る必要がない
- 再レンダー以外にステートの更新を伝えるsubscribe関数がある
- useReducer + Context よりも優れている点
- 冗長な定型コード(boilerplate)が少ない
- 不要な再描画が起きない
- 集中型のアクションベースのステート管理
まとめると、よりシンプルなRedux Toolkitです。
Redux Toolkit vs. zustand
という事で、同じアプリをzustandとRedux Toolkitで書いて比べてみましょう。いつものジャンケンアプリです。😊
1. store部分
まずは、Reduxの核であるStore部分のコードです。
- ① ⑪ Redux toolkitではステートの型にはステート値のみですが、zustandではActionも型に含まれます
- ② ⑫ Redux toolkitのcreateSliceがzustandではcreateになります
- zustandではmiddlewareが関数としてcreateにネストします
- immer(Immer)は破壊的なステート更新できるライブラリーで、Redux toolkitではデフォルトで入っています
- devtoolsはRedux DevToolsを使うためのmiddlewareです
- ③ ⑬ Redux toolkitのponアクションはReduxアクション用の引数が渡ってきますが、zustandは普通の関数です
- ④ ⑭ステートの更新、zustandではcreate(immer)の引数で渡ってくるset関数を使います
- ⑮ immerを使わない非破壊的ステート更新の場合は、このようなコードになります
- ⑤ Redux toolkitではアクションやリデューサーのexport用のコードが必要です
Redux Toolkit
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
export enum Jjudgment {
Draw = 0,
Win,
Lose
}
export enum Te {
Guu = 0,
Choki,
Paa
}
export type ScoreType = {
human: number;
computer: number;
judgment: Jjudgment;
};
export type PonPayload = Te;
export type ScoreState = ScoreType[]; // ← ①
let initialState: ScoreState = [];
const scoreSlice = createSlice({ // ← ②
name: "score",
initialState,
reducers: {
pon(state: ScoreState, action: PayloadAction<PonPayload>) { // ← ③
const human = action.payload;
const computer: Te = Math.floor(Math.random() * 3);
const judgment: Jjudgment = (computer - human + 3) % 3;
const score = { human: human, computer: computer, judgment: judgment };
state.push(score); // ← ④
}
}
});
export const { pon } = scoreSlice.actions; // ← ⑤
export default scoreSlice.reducer; // ← ⑤
zustand
import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';
import { devtools } from "zustand/middleware";
export enum Jjudgment { Draw = 0, Win, Lose }
export enum Te { Guu = 0, Choki, Paa}
export type ScoreType = {
human: number,
computer: number,
judgment: Jjudgment
}
export type ScoreState = { // ← ⑪
scores: ScoreType[];
pon: (human: Te) => void;
}
export const useStore = create( // ← ⑫
devtools(
immer<ScoreState>((set) => ({
scores: [],
pon: (human: Te) => { // ← ⑬
const computer:Te = Math.floor(Math.random() * 3);
const judgment:Jjudgment = (computer - human + 3) % 3;
const score = {human: human, computer: computer, judgment: judgment};
set(state => { state.scores.push(score); }); // ← ⑭
// set(state => ({scores: [...state.scores, score]})); // ← ⑮
}
}))
)
);
2. ステートの参照
ステートの参照を行うコンポーネントです。
- ① ⑪ ステートの参照は、ほぼ同じでRedux toolkitでは
useSelector
、zustandではuseStore
を使います
Redux Toolkit
import React from "react";
import { useSelector } from "react-redux";
import { ScoreState } from "./scoreSlice";
const ScoreBox: React.FC = () => {
const teString = ["グー", "チョキ", "パー"];
const judgmentString = ["引き分け", "勝ち", "負け"];
const scores = useSelector((state: ScoreState) => state); // ← ①
const tableStyle: React.CSSProperties = { marginTop: 20, borderCollapse: "collapse" };
const thStyle: React.CSSProperties = { border: "solid 1px #888", padding: "3px 15px" };
const tdStyle: React.CSSProperties = { border: "solid 1px #888", padding: "3px 15px", textAlign: "center" };
return (
<table style={tableStyle}>
<thead>
<tr>
<th style={thStyle}>あなた</th>
<th style={thStyle}>コンピュター</th>
<th style={thStyle}>勝敗</th>
</tr>
</thead>
<tbody>
{scores.map((score, ix) => (
<tr key={ix}>
<td style={tdStyle}>{teString[score.human]}</td>
<td style={tdStyle}>{teString[score.computer]}</td>
<td style={tdStyle}>{judgmentString[score.judgment]}</td>
</tr>
))}
</tbody>
</table>
);
};
export default ScoreBox;
zustand
import React from 'react';
import { useStore } from './store';
const ScoreBox: React.FC = () => {
const teString = ["グー","チョキ", "パー"];
const judgmentString = ["引き分け","勝ち", "負け"];
const scores = useStore(state => state.scores); // ← ⑪
・・・以下はRedux toolkitと同じ・・・
3. アクション
ステート更新を行う、アクションを呼び出すコンポーネントです。
- ① ⑪ Redux toolkitでは
useDispatch
を使ってアクションを呼び出します。zustandではuseStore
でステートからpon関数を取り出し使います - ② ⑫ Redux toolkitでは
useDispatch
を使ってアクションを呼び出しますが、zustandでは普通の関数呼び出しです
Redux Toolkit
import React from "react";
import { useDispatch } from "react-redux";
import { pon, Te } from "./scoreSlice";
const JyankenBox: React.FC = () => {
const divStyle: React.CSSProperties = { margin: "0 20px" };
const buttonStyle: React.CSSProperties = { margin: "0 10px", padding: "3px 10px", fontSize: 14 };
const dispatch = useDispatch(); // ← ①
return (
<div style={divStyle}>
<button onClick={() => dispatch(pon(Te.Guu))} style={buttonStyle}> // ← ②
グー
</button>
<button onClick={() => dispatch(pon(Te.Choki))} style={buttonStyle}>
チョキ
</button>
<button onClick={() => dispatch(pon(Te.Paa))} style={buttonStyle}>
パー
</button>
</div>
);
};
export default JyankenBox;
zustand
import React from 'react';
import { Te, useStore } from './store';
const JyankenBox: React.FC = () => {
const divStyle: React.CSSProperties = {margin: "0 20px"};
const buttonStyle: React.CSSProperties = {margin: "0 10px", padding: "3px 10px",
fontSize: 14};
const pon = useStore(state => state.pon); // ← ⑪
return (
<div style={divStyle}>
<button onClick={() => pon(Te.Guu)} style={buttonStyle}>グー</button> // ← ⑫
<button onClick={() => pon(Te.Choki)} style={buttonStyle}>チョキ</button>
<button onClick={() => pon(Te.Paa)} style={buttonStyle}>パー</button>
</div>
)
}
export default JyankenBox
4. アプリのメインコンポーネント
アプリのメインコンポーネントJyanken.tsx
は全く同じです。
Redux Toolkit & zustand
import JyankenBox from "./JyankenBox";
import ScoreBox from "./ScoreBox";
const Jyanken: React.FC = () => {
return (
<>
<h1>じゃんけん ポン!</h1>
<JyankenBox />
<ScoreBox />
</>
);
};
export default Jyanken;
5. エントリーポイント
Reactライブラリーから最初に呼び出されるコード。今回は開発環境にViteを使ったのでmain.tsx
です。
- ① Redux toolkitでは
configureStore
を使ってストアーを作っていますが、zustandにはありません - ② Redux toolkitを使うにはアプリを
<Provider>
コンポーネントで括る必要がありますが、zustandにはありません
Redux Toolkit
import React from "react";
import ReactDOM from "react-dom/client";
import Jyanken from "./Jyanken";
import logger from "redux-logger";
import { configureStore } from "@reduxjs/toolkit";
import { Provider } from "react-redux";
import scoreSlice from "./scoreSlice";
const store = configureStore({ // ← ①
reducer: scoreSlice,
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(logger)
});
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<Provider store={store}> // ← ②
<Jyanken />
</Provider>
</React.StrictMode>
);
zustand
import React from "react";
import ReactDOM from "react-dom/client";
import Jyanken from "./Jyanken";
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<Jyanken />
</React.StrictMode>
);
まとめ
Zustandは、とてもシンプルなReduxだとわかってって頂けましたでしょうか。
Redux Toolkitの導入には、まだRedux的な難しさがあると思います。
対してZustandは不思議なコードが少なく、わかりやすいReduxだと思います。Reduxの良さを低いコストで導入できる良いステート管理ライブラリーだと思います。
今回は出てきませんでしたが、API通信などの非同期アクションも通常の関数として書けます。
また、ドキュメントもZustand Documentation - Pmndrs.docsに良く書かれていると思います。迷ったときはRecipesを見ると解決するかもしれません。
Redux的なものを導入したくなった時に、Zustandは良い選択肢だと思います。