Zustandの基本構成とスライスパターンをコードベースで解説

# 動機

業務では状態管理にreduxを使うことがほとんどなのですが、正直reduxは面倒くさいことが多いです。

stateを1つ追加するだけでも書かなければならないコードが多く、学習コストもそれなりにかかるため、新しくプロジェクトにジョインしてくれた人に教育をするのも大変です。

何か別の状態管理ライブラリはないものかと探していた時に、Zustandを発見しました。

# Zustandとは

Zustandは、React用の軽量な状態管理ライブラリです。開発したのは、CSS-in-JSライブラリ「emotion」や、通知ライブラリ「react-toastify」などで知られる、Poimandresというチームです。

Zustandの特徴は、「少ないコードでシンプルに状態を管理できる」という点です。Reduxと比べると、非常に直感的で、設定にかかる手間がほとんどありません。

### 公式
https://zustand-demo.pmnd.rs/


# 使い方

### インストール

```bash
npm install zustand
```

もしくは

```bash
yarn add zustand
```

### 基本的なStoreの構成

```jsx
import { create } from 'zustand'

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

Storeは`create`関数を使って作成します。`bears`に加えて、これを変更するアクションも同じ場所で管理されています。

続いてStoreの値を呼び出してみましょう。

```jsx
function BearCounter() {
  const bears = useStore((state) => state.bears)
  return <h1>{bears} around here...</h1>
}
```

このように、簡単に呼び出すことができます。

Zustandによる状態管理の基本構成は以上です。reduxと比べてとても簡潔で、描かなければならないコードが少ないことを感じていただけたのではないでしょうか？

# スライスパターン

とはいえ、上記の基本構成では管理したいstateが増えると管理が大変になりそうです。そんな時はスライスパターンを使うことが推奨されています。

スライスパターンを採用することで、大きくなってしまったStoreを個別の小さなStoreに分割することができます。

スライスの書き方は以下の通りです。Storeを作った時と少し書き方が異なるだけで、ほぼ同じです。

### fishについてのStore

```jsx
export const createFishSlice = (set) => ({
  fishes: 0,
  addFish: () => set((state) => ({ fishes: state.fishes + 1 })),
})
```

### bearについてのStore

```jsx
export const createBearSlice = (set) => ({
  bears: 0,
  addBear: () => set((state) => ({ bears: state.bears + 1 })),
  eatFish: () => set((state) => ({ fishes: state.fishes - 1 })),
})
```

### 2つのStoreを合体

```jsx
import { create } from 'zustand'
import { createBearSlice } from './bearSlice'
import { createFishSlice } from './fishSlice'

export const useBoundStore = create((...a) => ({
  ...createBearSlice(...a),
  ...createFishSlice(...a),
}))
```

### stateの呼び出し

```jsx
import { useBoundStore } from './stores/useBoundStore'

function App() {
  // いずれもuseBoundStoreから呼び出すことができている
  const bears = useBoundStore((state) => state.bears)
  const fishes = useBoundStore((state) => state.fishes)
  const addBear = useBoundStore((state) => state.addBear)
  
  return (
    <div>
      <h2>Number of bears: {bears}</h2>
      <h2>Number of fishes: {fishes}</h2>
      <button onClick={() => addBear()}>Add a bear</button>
    </div>
  )
}

export default App
```

`const bears = useBoundStore((state) => state.bears)`のように呼び出すことで、無関係なstateが更新されても再レンダリングが起きないように管理されています。

# TypeScript対応

ZustandはTypeScriptにも対応しています。

### 基本構成の場合

```tsx
import { create } from 'zustand'

interface BearState {
  bears: number
  increase: (by: number) => void
}

const useBearStore = create<BearState>()((set) => ({
  bears: 0,
  increase: (by) => set((state) => ({ bears: state.bears + by })),
}))
```

`create`関数にジェネリクスでstateの型を渡しています。また、型の後に「（）」が追加されています。これはカリー化が行われていることを意味します。

ここでカリー化が行われている理由は、型推論の精度をあげるためだそうです。詳しくは公式ページでの解説を参照ください。

https://zustand.docs.pmnd.rs/guides/typescript#slices-pattern:~:text=Why%20the%20currying%20()(...)%3F

### スライスパターンの場合

```tsx
import { create, StateCreator } from 'zustand'

interface BearSlice {
  bears: number
  addBear: () => void
  eatFish: () => void
}

interface FishSlice {
  fishes: number
  addFish: () => void
}

interface SharedSlice {
  addBoth: () => void
  getBoth: () => void
}

const createBearSlice: StateCreator<
  BearSlice & FishSlice,
  [],
  [],
  BearSlice
> = (set) => ({
  bears: 0,
  addBear: () => set((state) => ({ bears: state.bears + 1 })),
  eatFish: () => set((state) => ({ fishes: state.fishes - 1 })),
})

const createFishSlice: StateCreator<
  BearSlice & FishSlice,
  [],
  [],
  FishSlice
> = (set) => ({
  fishes: 0,
  addFish: () => set((state) => ({ fishes: state.fishes + 1 })),
})

const createSharedSlice: StateCreator<
  BearSlice & FishSlice,
  [],
  [],
  SharedSlice
> = (set, get) => ({
  addBoth: () => {
    // すでに定義されているものを使いまわすこともできます
    get().addBear()
    get().addFish()
    // 使いまわさない場合は以下のように書くこともできます
    // set((state) => ({ bears: state.bears + 1, fishes: state.fishes + 1 })
  },
  getBoth: () => get().bears + get().fishes,
})

const useBoundStore = create<BearSlice & FishSlice & SharedSlice>()((...a) => ({
  ...createBearSlice(...a),
  ...createFishSlice(...a),
  ...createSharedSlice(...a),
}))

```

`createBearSlice`, `createFishSlice`, `createSharedSlice`の型として`StateCreator`が渡されており、`StateCreator`にはジェネリクスで4つのものが渡されています。

1. **state全体の型**
    そのスライスが依存している他のスライスの型を渡します。
    例えば`createBearSlice`はbearとfishに関係するstateを扱う必要があるので、`BearSlice & FishSlice`が渡されています。もしe`atFish()`がなかったら、`FishSlice`を渡す必要はありません。
    
2. **ミドルウェアの配列**
    ミドルウェアを使いたい場合はここに配列を渡すことがあります。使わない場合は空の配列を渡します。
    例えばloggerを使いたい場合、`[["zustand/devtools", never]]`を渡します。
3. **ストアミューテラの配列**
    ストアの振る舞いをカスタマイズするためのもの。ストアの作成時に追加の機能やロジックを適用したい場合に配列を渡します。
    例えばdevtoolsを使いたい場合、`[["zustand/devtools", never]]`を渡します。
4. **stateとアクションの型**
    このスライスで作成するstateとアクションの型を渡します。
    例えば`createBearSlice`はbearに関するstateとアクションなので、`BearSlice`型が渡されています。

# ReduxとZustandの特徴比較

最後にreduxとzustandの特徴比較表を置いておきます。参考になれば幸いです。

| 特徴 | Zustand | Redux |
| --- | --- | --- |
| **ボイラープレート** | 少ない | 多い |
| **学習コスト** | 低い | 高い |
| **状態管理の規模** | 小規模・中規模向き | 大規模向き |
| **パフォーマンス** | 効率的な再レンダリング | 最適化が必要な場合がある |
| **ツールサポート** | 限定的 | 豊富な公式ツール・DevTools |

# 参考

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

https://reffect.co.jp/react/zustand#google_vignette