🐻

【React状態管理】Zustandでストアを複数コンポーネントで使い回す

2023/02/22に公開

はじめに

状態管理ライブラリを使用していて、ストア(状態とアクションのセット)を複数コンポーネントで使い回したい!
ということはありませんか?

この記事では Zustand という React の状態管理ライブラリを使用して、同じストアを複数コンポーネントで使いまわす例を紹介します。

Zustand とは

https://zustand-demo.pmnd.rs/

こちらの記事がとてもわかりやすいです。感謝。
https://zenn.dev/kira05219/articles/eab5fd7d7d846b

Zustand は、React Developer ロードマップ でも 2023 年 2 月現在、推奨の状態管理ライブラリとして指定されています。
https://roadmap.sh/react

バージョン情報

https://github.com/YamabikoLab/zustand-sample-context/blob/feature/base/package.json#L11-L14

基本形

ボタンをクリックすると、カウンターがインクリメントされるというシンプルなサンプルです。

https://codesandbox.io/embed/github/YamabikoLab/zustand-sample-context/tree/feature%2Fbase/?fontsize=14&hidenavigation=1&module=%2Fsrc%2Fstore%2FuseMyStore.tsx&theme=dark

シンプルなストアの実装です。
https://github.com/YamabikoLab/zustand-sample-context/blob/feature/base/src/store/useCounterStore.tsx

複数 NG パターン

次に NG パターンを見てみましょう。
ボタンをクリックすると、両方の数値がインクリメントされてしまいます。
同一のストアを参照しているからです。

https://codesandbox.io/embed/github/YamabikoLab/zustand-sample-context/tree/feature%2Fng/?fontsize=14&hidenavigation=1&module=%2Fsrc%2FApp.tsx&theme=dark

複数 OK パターン

そして、複数のコンポーネントの状態を独立して管理できている OK パターンです。

https://codesandbox.io/embed/github/YamabikoLab/zustand-sample-context/tree/feature%2Fok/?fontsize=14&hidenavigation=1&module=%2Fsrc%2Fstore%2FuseMyStore.tsx&theme=dark

ストアを生成するのではなく、ストアを生成する関数式を定義しています。
https://github.com/YamabikoLab/zustand-sample-context/blob/feature/ok/src/store/useCounterStore.tsx#L16-L25

そして、コンテキストとプロバイダを定義します。
プロバイダの中でストアを生成している点にご注目ください。
https://github.com/YamabikoLab/zustand-sample-context/blob/feature/ok/src/store/useCounterStore.tsx#L27-L40

それぞれのカウンターにプロバイダでストアを供給しています。
このようにして、独立したストアを提供している訳です。
https://github.com/YamabikoLab/zustand-sample-context/blob/feature/ok/src/App.tsx#L8-L18

subscribe

OK パターンでは subscribe メソッドを呼び出すことができません。
subscribe メソッドを呼び出せるように追加の実装が必要です。

https://codesandbox.io/embed/github/YamabikoLab/zustand-sample-context/tree/feature%2Fsubscribe/?fontsize=14&hidenavigation=1&module=%2Fsrc%2Fcomponents%2FCounter.tsx&theme=dark

StoreApi 型のストアを取得するメソッドを追加します。
https://github.com/YamabikoLab/zustand-sample-context/blob/feature/subscribe/src/store/useCounterStore.tsx#L50-L56

subscribe メソッドを呼び出せるようになりました。
ボタンをクリックするとコンソールにログ出力します。
https://github.com/YamabikoLab/zustand-sample-context/blob/feature/subscribe/src/components/Counter.tsx#L6-L9

Redux Devtools

Zustand では Redux Devtools を使用して、状態を追跡することが可能です。
devtools メソッドを実装します。

https://github.com/YamabikoLab/zustand-sample-context/blob/feature/devtools/src/store/useCounterStore.tsx#L17-L28

Redeux DevTool で確認すると、異なるストアで状態管理していることを確認できます。
残念ながら codesandbox 上では正常に動作しないようなので、ここではソースコードの改修箇所のご紹介に留めておきます。

まとめ

ストア(ステートとアクションのセット)を使いまわしたい。
きっと、誰でも一度はそう思いますよね(?)

機能として提供されていても良さそうなのですが、ドキュメントのどこにもそのような記述を見つけることができませんでした。

Redux など他の状態管理ツールや issue なども調査しました。
同様の問題を抱えている開発者は少なくありませんでした。

しかし、Zustand を除くライブラリ開発サイドからの回答では、「配列を使え」とか「マップにしてキー管理するように」などのベタな回答しか見つけることができませんでした。

ところが、Zustand の issue にて、今回の記事でご紹介したようなサンプルを見つけることができました。

サンプルコードを提示してくださっていたのは Jotai 開発者でもあられる daishi さんです。
https://zenn.dev/dai_shi

今回の記事ではそのサンプルを元に、TypeScript に置き換え、型の定義を明確にし、全体的に整理をしました。

さらに、devtools の実装方法も追加しました。

尚、この例ではコンテキストとプロバイダを使用していますが、一般的にはコンテキストやプロバイダは使用しません。
今回は複数コンポーネントで使いまわしたいという目的だったので、コンテキストとプロバイダを使用しています。
Zustand を使っていて、プロバイダが積み重なる・・・ということには通常ならないのでご安心ください。

サンプルコードを提示してくださった daishi さんに心から感謝致します。

参考資料

https://zustand-demo.pmnd.rs/

https://github.com/pmndrs/zustand/issues/128

https://codesandbox.io/s/polished-pond-4jn1e2

https://github.com/YamabikoLab/zustand-sample-context

Discussion