zustandを初めて使ってみた


# はじめに

reactの状態管理ライブラリのzustandを今更ながら初めて知り、以前recoilを使っていましたがzustandの方がシンプルに状態管理できそうだったのでキャッチアップ&実務で活用してみました。
今回はzustandの大まかな紹介になります。

## zustandとは

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/924294/c8b91a84-24fa-c929-9785-22fb520993ee.png" width="50%" />

公式ドキュメント：https://zustand-demo.pmnd.rs/

冒頭でも軽く説明していますが、Zustandは状態管理ライブラリであり**小さく**、**シンプル**に管理することができる。
またミドルウェアのサポート(永続化、デバック、非同期など)がいくつか提供されている。
そしてアプリケーション全体にアクセスできる**グローバルステート**の管理ができます。

zustandはドイツ語で「状態」を表している。開発元はドイツの「Poimandres」という会社でJSのライブラリをいくつか開発しており、zustandと同じ状態管理ライブラリの`Jotal`やアニメーションライブラリの`React Spring`などあります。

###  ちょっとした歴史

2019年頃に開発されておりReact hooksを活用してシンプルで直感的なstate管理を提供することを目指していた。
また軽量ライブラリの中ではこの一年で使用率が伸びており比較人気が上がっているのもみれたり、最新バージョンは`v4.3.8`（2023/6/12時点）と定期的にメンテナンスがされていて手厚い。
以下は最新のnpmトレンドのものです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/924294/7a7d3dde-24e9-1c31-97bc-df41eed8f443.png)

（Reduxは相変わらず人気があり、ダウンロード数で言えばzustandの4倍近くありました。）


## 使用例

zustandの`create`メソッドを使うことで状態管理のストアを作成することができる。
stateの初期値の設定とcreateメソッドに以下の関数が備わっています。

- `get()`： storeの値を参照する
- `set()`： 状態を更新する

上記があるのでstoreの中で状態と更新ロジックをstoreの中で作成することができます。


```create-example.ts

import { create } from 'zustand'

// stateの定義と更新ロジックを含むストアを作成。
const useStore = create(set => ({
  count: 0,
  increase: () => set(state => ({ count: state.count + 1 })),
  decrease: () => set(state => ({ count: state.count - 1 }))
}))

// ストアをコンポーネントで使用
function Counter() {
  const { count, increase, decrease } = useStore()
  return (
    <div>
      <button onClick={decrease}>-</button>
      <span>{count}</span>
      <button onClick={increase}>+</button>
    </div>
  )
}


```

また、ストアの外で状態の更新を定義することが可能です。

```diff_typescript

import { create } from "zustand";

const useStore = create(set => ({
  count: 0,
  increase: () => set(state => ({ count: state.count + 1 })),
  decrease: () => set(state => ({ count: state.count - 1 }))
}))

+ export const reset = () => useStore.setState({ count: 0 });


```

### store利用の注意点

ストアのstateを丸ごと呼び出すと変更があるたびに再レンダリングされてしまうので利用する時は注意する。
なのでstoreを使う時は、state毎の指定が必要になります。

storeを利用する時には使いたいstate、set関数をセレクターを引数に定義することで、必要な状態のみを返す形になります。

```create-example.ts

import { create } from 'zustand'


const useStore = create(set => ({
  count: 0,
  increase: () => set(state => ({ count: state.count + 1 })),
  decrease: () => set(state => ({ count: state.count - 1 })),
}))

// increaseのみを購読する形になる。
function CountUp() {
  const increase = useStore((state) => state.increase)

  return (
    <div>
       <button onClick={increase}>+</button>
    </div>
  )
}


```


Zustand内で非同期処理を使ってアクションもすることが可能です。
例えばデータ取得で呼び出すfetchとステートの変更と一緒に使用することも可能です。

```fetch-example.ts

import create from 'zustand'

const useStore = create((set) => ({
  data: null,
  loading: false,
  error: null,
  getData: async () => {
    set({ loading: true })
    try {
      const res = await fetch('https://example.com/data')
      const data = await res.json()
      set({ data, loading: false })
    } catch (error) {
      set({ error, loading: false })
    }
  }
}))

// コンポーネント内で使用
function MyComponent() {
  const { data, loading, error, getData } = useStore()

  ///
}


```



## `presist` 永続化

`persist`とというstateを永続化するためのミドルウェアがあります。storeを作る時に`persist`を使うことでstateを`localStorage`に保存してブラウザがリロードされてもstateを保持することができます。

使う時はcreate内のstoreをpersistでラップして、ストレージに保存するKeyの`name`を追加する必要がある。
デフォルトでは保存先がlocalStorageとなっていますが任意のStorageを指定することも可能。

```persist-example.ts

import create from 'zustand'
import { persist } from 'zustand/middleware'

const useStore = create(
  persist(
    set => ({
      count: 0,
      increase: () => set(state => ({ count: state.count + 1 })),
      decrease: () => set(state => ({ count: state.count - 1 })),
    }),
    {
      name: 'count-store', // storageのキー名
      // getStorage: () => sessionStorage, // カスタムストレージを指定することも可能。
    }
  )
)


```


## devtool

devtoolも提供されており、zustandの状態やアクションのトラッキング、デバックなどができます。
使う場合はcreate内のstoreを`devtools()`でラップして使います。
拡張機能については[Redux Developer Tools](https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd?hl=ja)で確認することができます。

```devtool.ts

import create from 'zustand'
import { devtools } from 'zustand/middleware'

const useStore = create(
  devtools(set => ({
    count: 0,
    increase: () => set(state => ({ count: state.count + 1 })),
    decrease: () => set(state => ({ count: state.count - 1 })),
  }))
)

```

実際にカウントのstateをRedux Developer Toolsで確認できました。
![スクリーンショット 2023-06-19 0.39.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/924294/74556bde-eddb-1e06-81b2-7326996ef30e.png)


## その他

### スライスパターン

スライスパターンという形で個々の状態と更新を小さなstoreとしてを分割し利用することも可能です。
以下では`count`と`color`のstateを分けて`create()`で集約させている形にしています。

```slice-exmaple.ts

import { create, StateCreator } from 'zustand'

const createCountStore: StateCreator = () => ({
  count: 0,
  increment: () => set(state => ({ count: state.count + 1 })),
  decrement: () => set(state => ({ count: state.count - 1 })),
});

const createColorStore: StateCreator = () => ({
  color: 'blue',
  setColor: (color) => set({ color }),
});

const useStore = create(set => ({
  ...createCountStore(set),
  ...createColorStore(set),
}))

// コンポーネント内で使用
function Counter() {
  const { count, increment, decrement } = useStore();

  ///
}

function Color() {
  const { color, setColor } = useStore();

  ///
}


```



###  様々なサードパーティツール

https://github.com/pmndrs/zustand/blob/main/docs/integrations/third-party-libraries.md

- [simple-zustand-devtools](https://github.com/beerose/simple-zustand-devtools)： react-devtoolでストアの状態を確認することが出来る。
- [zustand-forms](https://github.com/Conduct/zustand-forms)： formのstateやバリデーションをストアで管理できる。
- [vue-zustand](https://github.com/wobsoriano/vue-zustand)： Vueでzustandを扱える（Angularやsoild.js対応もあった）

### zustandの良かったところ
- シンプルで簡潔的なストア管理ができる
- 柔軟な状態の管理
- Devtoolやpersistなどのミドルウェアのサポート
- TypeScriptのサポート
- React以外のフレームワークでも使用が可能