状態管理ライブラリのzustand使ってみた

# はじめに

zustandはコンパクトなReactで動作する状態管理ライブラリです。
ミドルウェアで必要な分だけ機能拡張も可能です。

Redux、Recoilなど他の状態管理ライブラリと比較すると
最初に覚える概念が少なくContext.Providerでラップしなくてよかったりと
導入しやすいライブラリだと思います。

今回はzustandを使ってみたまとめになります。

https://github.com/pmndrs/zustand

# インストール

```typescript
npm install zustand
```

# 基本的な使い方

ストアを作成してそれを呼び出すだけなのでとても簡単です。

## ストア作成

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

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

```

## コンポーネントにバインド

```typescript
function Controls() {
  const increase = useBearStore((state) => state.increase)
  return <button onClick={increase}>one up</button>
}
```

# 複数の状態を持っている場合の取得

## 個別に取得

基本的には必要な状態のみ取得するようにします。
そうすることで不要な再レンダリングを防げます。

```typescript
const nuts = useBearStore((state) => state.nuts)
const honey = useBearStore((state) => state.honey)
```

## 複数の状態を同時に取得

ネストしていないフラットな複数の状態を同時に取得する場合はshallowを指定することで
指定されていない状態が更新されたときの再レンダリングを防ぐことが出来ます。

shallowを指定しない場合useBearStoreにbearsというもう一つの状態があり更新されたとき
下記も再レンダリングされます。

```typescript
const { nuts, honey } = useBearStore(
  (state) => ({ nuts: state.nuts, honey: state.honey }),
  shallow
)
```

再レンダリングをより詳細に制御するには、任意のカスタム等値関数を作成します。

```typescript
const treats = useBearStore(
  (state) => state.treats,
  (oldTreats, newTreats) => compare(oldTreats, newTreats)
)
```

# 状態の更新

ReactのuseStateと同様に自動的な更新オブジェクトのマージを行いません。
下記のuseStateの例のようにマージしたい場合、スプレッド構文と併用します。
zustandのset関数も同様に状態を保持する必要があります。

```typescript
const [state, setState] = useState({});
setState(prevState => {
  return {...prevState, ...updatedValues};
});
```

zustandでストアを作成する時の典型的な例では下記のような形になります

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

const useCountStore = create((set) => ({
  count: 0,
  inc: () => set((state) => ({ count: state.count + 1 })),
}))
```

set関数は保持している値にマージするために本来はこのような形になるはずです。

```typescript
set((state) => ({ ...state, count: state.count + 1 }))
```

ただし、これは一般的なパターンであるため
...state部分をスキップできるので最終的に下記だけになります。

```typescript
set((state) => ({ count: state.count + 1 }))
```

## ネストされたオブジェクト

set関数は、１つのレベルでのみ状態をマージします。
ネストされたオブジェクトがある場合は、それらを明示的にマージする必要があります。
例えば次のようにスプレッド演算子を使用します。

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

const useCountStore = create((set) => ({
  nested: { count: 0 },
  inc: () =>
    set((state) => ({
      nested: { ...state.nested, count: state.nested.count + 1 },
    })),
}))
```

さらに深くネストした状態を持っている場合はスプレッド演算子を各レベルで書く必要があり
冗長でコードが長くなるのでimmerミドルウェア等を利用します。

https://github.com/immerjs/immer

# ベストプラクティス

１つの大きなストアを作りその中に小さなストアを設置するパターンが推奨されています。

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

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

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

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

# まとめ

zustandは簡単に導入出来てとても使いやすいと感じました。
気軽に導入出来る分、使い方を工夫しないと予期しない状態変更になったりすると思うので
プロジェクト内で使い方を予め定義して導入すると良さそうです。
引き続きzustandの情報収集はやっていきたいと思っています。