目次
Zustandとは?
ZustandはFluxの原則に従って作られた、シンプルなJavascriptの状態管理ライブラリです。もともとドイツ語で読み方は「ツーシュタント」、意味は「状態」です(そのまんまですね。。)。Github上で16.8Kものスターを獲得していて、すでに人気のあるライブラリになりつつあります。
Javascriptの状態管理ライブラリなので、VanillaJSでもAngular、Vue、ReactなどのJavascriptフレームワークでも使えます。
もし状態管理ライブラリとしてReduxを使用したことがあれば、このライブラリはよく似ているので親しみやすいと思います。
使用におけるメリットとデメリット
メリット
- ・Reduxと比較し、コードの量が少なく書ける
すでにご存知の方もいらっしゃると思いますが、Reduxは非常に有益な状態管理ライブラリである代わりに使用にあたって多くのコードを書く必要があります。ですので一般的には、Reduxは複数の異なるステートを管理し、かつ状態が頻繁に変更されるような比較的大きいプロジェクトに適していると言われます。一方で、Zustandは使用にあたって多くのコードを必要としません。ReduxはProviderコンポーネントで親コンポーネントをラップする必要がありますが、そういった仕様もありません。快適ですね。
- ・シンプルなドキュメント
Reduxの公式サイトに行くとわかりますが、Reduxは様々な使用例を含んだ豊富なドキュメントがあります。よく読めば様々なことができるのですが、いかんせん量が多いため小さいプロジェクトだとやりすぎな印象があります。一方Zustandのドキュメントは、簡単なチュートリアルとシンプルですが様々なコード例で構成されており、非常に読みやすいです。小さなプロジェクトではZustandのもので十分だと思います。
- ・柔軟性
2点目で公式サイトに様々なコード例が掲載されていることに触れましたが、Zustandでは様々な書き方ができます。個人的には、これがZustandの最も特徴的な部分だと考えています。シンプルに書くこともできますし、不変性を担保するためにミドルウェアとしてimmerを使用することもできます。Reduxの書き方が好きであれば、reducerやdispatchを使用したReduxの書き方もできます。Reduxは1つの巨大なグローバルステートを持ちますが、Zustandではいくつかにステートを分割してストアに入れ、それぞれ独立したものとして扱うことができます。もちろんReduxと同じように1つのグローバルステートを作成することもできますし、別々のものとして作ったストアをRedux toolkitのようにスライス化して、それを統合することも可能です。もちろんTypescriptにも対応しています。Reduxの良いところを受け継ぎつつ、シンプルにし、柔軟性を持たせたという感じですね。素晴らしいと思います。
デメリット
- ・あまり多くの情報がない
状態管理系のライブラリはその重要性から様々なものが出てきており、Zustandだけを使用する人は多くありません。言い換えれば、それほど多くの情報が得られないということです。例えばRedux及びRedux toolkitは現在のデファクトスタンダードの1つなので多くの情報があり、かつ多くの人が様々なイシューやエラーに悩まされてきているので、困った時の解決法や記法が豊富にあります。Zustandの場合はそれらが少ないため、自身で試行錯誤しながら解決しなければならない場面が多くなると考えられます。
- ・カッコ(ブラケット)の数が多い
私見ですが、Zustandはコードの量(行)を減らすために、多くのカッコを必要とします。記事の後半でコード例も載せるので、詳しくはそちらをご覧ください。シンプルなカウンターアプリ作成などだとあまり感じないのですが、データフェッチなど状態管理が多少複雑になるものだと、少し読みにくさを感じます。代わりにコード量が減り、スクロールの回数も減るので完全に個人の好みだと考えています。
- ・ベストな記法を探すのが難しい
メリットの部分で柔軟性、色々な書き方ができる点を挙げましたが、言い換えると自分でどのような書き方をするか決めなければなりません。チームで開発する場合、書き方のルールを設定する必要も出てくるでしょう。これがベスト、というのも今のところ定まっていないので、自分で手探りでより良い方法を模索していく形になるかもしれません。シンプルなアプリだとそれで良いですが、複雑なアプリを作る場合は多少トライアンドエラーのコストがかかると考えています。
基本情報は十分だと思いますので、ここからは具体例に移ります。
Reactと共にZustandを使ったシンプルなカウンターアプリ、及びデータフェッチの例となります。
例1(カウンターアプリ)
以下が、今回作るアプリのフォルダ構成です(必要最低限のものしか記載していません)
src ├── App.css ├── App.js ├── app │ └── store.js ├── features │ └── counter │ └── Counter.js ├── index.css ├── index.jsCopy
ステップ
- 1. Create react appを使ってベースとなるReactのアプリを作成し、作成したディレクトリに移動します。
npx create-react-app zustand-counter(名前は自由に決めてください) cd zustand-counter
- 2. Zustandをインストールします。
npm install zustand
- 3. Counter.jsファイルをfeatures/counterフォルダ内に作成、格納します。フォルダ構成は自由なので、好みのやり方でどうぞ。
- 4. 作成したCounter.jsファイルをApp.jsにインポートします。この時点でCounter.jsファイルはまだ空で構いません。App.jsの中身は以下のような形になります。
import "./App.css";
import Counter from "./features/counter/Counter";
function App() {
return (
<div className="App">
<h1>Zustand</h1>
<Counter />
</div>
);
}
export default App
- 5. store.jsファイルをappフォルダの中に作成、格納してください。こちらもフォルダ構成は自由です。Zustandをstore.jsにインポートし、useStoteとして使用します。以下のようにストアを作成し、デフォルトの状態及びメソッドを定義してください。Reduxと同じようにpayloadを使うと柔軟にカウントの幅を決めることができます。ちなみにこのpayloadは必ずしもpayloadと命名しなければならないわけではなく、公式ではbyを使っていました。私はReduxに習ってpayloadを使ったのですが、ここもお好みでどうぞ。
import create from "zustand";
export const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
incrementByAmount: (payload) =>
set((state) => ({ count: state.count + payload })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
}));
- 6. 5で作成したuseStoteをCounter.jsファイルにインポートし、使用します。このuseStoreはReduxのuseSelectorのような使い方ができ、(oldValue) => (newValue)のように使用します。もしpayloadを使用する場合は、使用する際にコールバック関数を用いる必要があるのでそちらもご注意ください。
import { useStore } from "../../app/store";
import React from "react";
const Counter = () => {
const count = useStore((state) => state.count);
const increment = useStore((state) => state.increment);
const incrementByAmount = useStore((state) => state.incrementByAmount);
const decrement = useStore((state) => state.decrement);
const reset = useStore((state) => state.reset);
return (
<div>
<div>{count}</div>
<button onClick={increment}>increment</button>
<button onClick={() => incrementByAmount(5)}>incrementByAmount</button>
<button onClick={decrement}>decrement</button>
<button onClick={reset}>reset</button>
</div>
);
};
export default Counter
- 7. 以下が完成版となります。globalのステートなので、別のコンポーネントで使用した場合もステート情報は共有されます。もしuseStoreではない名前のストアを作成すると、内部が全く同じコードでも別々のステートとして扱われますので、必要に応じて役立ててください。
例2:(データフェッチアプリ)
以下が、今回作るアプリのフォルダ構成です(例1と同様に、必要最低限のものしか記載していません)
src ├── App.css ├── App.js ├── app │ └── store.js ├── features │ └── fetchData │ └── FetchDataZustand.js ├── index.css ├── index.jsCopy
ステップ
- 1. ベースとなるReactのアプリをCreate react appで作成します。
npx create-react-app zustand-fetch-data(名前は自由に決めてください) cd zustand-fetch-dataCopy
- 2. ZustandとAxiosをインストールします。もしビルトインのFetch functionを使用する場合は、Axiosのインストールは不要です。その場合はAxiosパートは適宜置き替えてください。
npm install zustand axios
- 3. FetchDataZustand.jsファイルをfeatures/fetchDataフォルダ内に作成、格納してください。こちらのフォルダ構成も任意です。
- 4. FetchDataZustand.jsファイルをApp.jsにインポートしてください。以下のような形になります。
import "./App.css";
import FetchDataZustand from "./features/fetchData/FetchDataZustand";
function App() {
return (
<div className="App">
<h1>Zustand</h1>
<FetchDataZustand />
</div>
);
}
export default App
- 5. store.jsファイルをappフォルダの中に作成、格納してください(フォルダ作成は任意です)。ZustandとAxiosをインポートして使用します。やり方は色々ありますが、基本的にReduxの方法を踏襲しています。data、loading、errorの3つのステートを作成し、それらのステートをfetch functionの中で適宜入れ替えています。多少コード量は増えますが、それでもReduxよりは少なくなります。今回はデモ目的でjsonplaceholderのエンドポイントをフェッチ先として指定していますが、もちろんどこをフェッチ先にしても問題なく動きます。
import create from "zustand";
import axios from "axios";
const useStore = create((set) => ({
data: [],
loading: false,
hasErrors: false,
fetch: async () => {
set(() => ({ loading: true }));
try {
const response = await axios.get(
"https://jsonplaceholder.typicode.com/users/1"
);
set((state) => ({ data: (state.data = response.data), loading: false }));
} catch (err) {
set(() => ({ hasErrors: true, loading: false }));
}
},
}));
export default useStore
- 6. 5で作成したuseStoreをFetchDataZustand.jsにインポートし、使用してください。例1と同じ様な使い方になります。Reduxと違うところは、このuseStoreがuseSelectorとuseDispatchを兼ねているところです。作成した機能をuseSelectorと同様の方法で指定すると、それをそのまま機能として使用できます。
import useStore from "../../app/store";
import React from "react";
const FetchDataZustand = () => {
const data = useStore((state) => state.data);
const loading = useStore((state) => state.loading);
const hasErrors = useStore((state) => state.hasErrors);
const fetchData = useStore((state) => state.fetch);
if (loading) {
return <p>Loading</p>;
}
if (hasErrors) {
return <p>cannot read data</p>;
}
return (
<>
<div>
<button onClick={fetchData}>Fetch and Set a data of zustand</button>
</div>
<div>{data.name}</div>
</>
);
};
export default FetchDataZustand
- 7. 以下が実際のデモアプリです。データが軽いのでローディングステートは出てませんが、ちゃんとデータが取れているのがわかります。
例えばエンドポイントのコードをコメントアウトしてデータが取れないようにすると、ちゃんとエラーステートが動いていることがわかります。一瞬ですがローディングもちゃんとされていることがわかります。
まとめ
Zustandは最も便利なJavascriptの状態管理ライブラリの1つで、これを使うとより便利に、かつコード量が少なく状態管理が可能です。もし1つのグローバルストアでなく独立した複数のストアが欲しいと考えた時は、このライブラリが有益だと考えています。
参考資料
Official documentation:https://docs.pmnd.rs/zustand/introduction
Slice example:https://github-wiki-see.page/m/pmndrs/zustand/wiki/Splitting-the-store-into-separate-slices
Zustand's Guide to Simple State Management: https://blog.bitsrc.io/zustands-guide-to-simple-state-management-12c654c69990
Reactで状態管理 初心者でも簡単Zustandの設定方法:https://reffect.co.jp/react/zustand
ここまでお読みいただきありがとうございました!