RecoilからZustandへ移行: ステート管理の簡素化

## はじめに
Reactプロジェクトでのステート管理に、ReduxとRecoilの両方を使用してきました。
しかし、Reduxを用いた実装から数年が経過し、処理が複雑化して開発のネックになっていました。

途中からRecoilに移行していましたが、Recoilには一つ惜しい点がありました。それは、Reactコンポーネント内でしか呼び出せないため、JavaScriptやTypeScriptファイルから直接呼び出すことができないことです。

例えば、自作のカスタムフック内からRecoilのステートを使用することはできません。

```ts
const useMyCustomHook = () => {
  // Error: Invalid hook call. Hooks can only be called inside of the body of a function component
  const [text, setText] = useRecoilState(textState);
}
```
このように呼び出そうとするとエラーが発生します。

:::note alert
Error: Invalid hook call. Hooks can only be called inside of the body of a function component. 
:::

これはReactのフック関数のルールなので仕方ありません。

https://ja.legacy.reactjs.org/docs/hooks-rules.html#only-call-hooks-from-react-functions

ReduxのAction関数のように、コンポーネント外からでも呼び出しを行いたいため、他のライブラリへの移行を検討することにしました。

## 本題
そこで採用したのがZustandです。

https://github.com/pmndrs/zustand

### Zustandとは
Zustandは、Reactアプリケーションでの状態管理をシンプルに行うための軽量なライブラリです。Recoilと比べて、以下のような特徴があります：

- **シンプルさと軽量さ**: Zustandは小さなサイズでありながら強力な機能を提供します。設定や構成が少なく、すぐに使い始めることができます
- **使用範囲の広さ**: 状態管理のためのグローバルストアを簡単に作成でき、Reactコンポーネント外からもステートを直接呼び出すことができます
- **ミドルウェアの柔軟性**: Reduxのようなミドルウェア機能もサポートしており、デバッグや永続化が容易に行えます

RecoilではReactコンポーネント内でしかステートを呼び出せない制約がありましたが、Zustandではカスタムフックやその他の関数内でもステートを操作できる点が大きなメリットです。

### 導入方法
```shell
npm install zustand
```
### Zustandの使用方法
#### ストアを定義する
Zustandではストアを定義して使用することができます。ReduxのProviderに比べると簡単に実装できます。
ストアにはステートの初期値やAction処理を追加します。定義したステート管理は`create`関数でフック化し、アプリ内で使用できるようにします。

データを永続化するには`persist`を使用します。ストレージにつける名前はアプリ内で一意になるように定義します。名前が重複すると正しく永続化できなくなるため注意が必要です。

``` ts
import { create } from 'zustand'
import { devtools, persist } from 'zustand/middleware'
import type {} from '@redux-devtools/extension' // required for devtools typing

// ストア情報を定義するinterface
interface BearState {
  bears: number
  increase: (by: number) => void
}

// ステート管理フック
const useBearStore = create<BearState>()(
  devtools(
    persist(
      (set) => ({
        bears: 0,
        increase: (by) => set((state) => ({ bears: state.bears + by })),
      }),
      {
        name: 'bear-storage',
      },
    ),
  ),
)
```

#### コンポーネントからステートを呼び出す

定義したフックから必要なステートとActionを指定することで、取得したいステートを使用できます。

```tsx
function BearCounter() {
  // useBearStoreからステートbearsを取得
  const bears = useBearStore((state) => state.bears)
  return <h1>{bears} around here ...</h1>
}

function Controls() {
  // useBearStoreからアクションincreasePopulationを呼び出し
  const increasePopulation = useBearStore((state) => state.increasePopulation)
  return <button onClick={increasePopulation}>one up</button>
}
```

##### コンポーネント外の関数からActionを呼び出す

ステート管理用フックとは別に、必要なステートやActionを関数化して公開することで、コンポーネント外からも呼び出すことができます。

```ts
// Actionをラップした関数を公開する
export const increaseBears = (by: number) => {
  const { increase } = useBearStore.getState();
  increase(by);
};
```

``` ts
// Actionを読み込み、関数内から呼び出す
import { increaseBears } from 'zustand/bearStore';

const useMyCustomHooks = () => {
  const increasePopulation = (by: number) => {
    increaseBears(by);
  };
  return {
    increasePopulation,
  };
};
```

### 肥大化しそうなStoreはSliceで分割する

ステートが増えてStoreが肥大化そうな場合は、Sliceを使用してStoreを分割することで可読性が向上します。

詳細については、[こちらのドキュメント](https://github.com/pmndrs/zustand/blob/main/docs/guides/typescript.md#slices-pattern)を参照してください。

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

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

interface FishSlice {
  fishes: number
  addFish: () => 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 })),
})

// BearSliceとFishSliceをまとめるStore
const useBoundStore = create<BearSlice & FishSlice & SharedSlice>()((...a) => ({
  ...createBearSlice(...a),
  ...createFishSlice(...a),
}))
```

## まとめ
ReduxとRecoilからZustandへの移行を通じて、プロジェクトのステート管理の複雑さを大幅に軽減することができました。
特に、コンポーネント外からステート操作が可能になったことは、非常に使い勝手が良いです。

Zustandを導入したことで、ReduxやRecoilの使用時と比較して、開発の柔軟性が向上し、今後のスムーズな開発が期待できます。

## 参考リンク

- [Zustand](https://github.com/pmndrs/zustand)
- [Redux](https://redux.js.org/)
- [Recoil](https://recoiljs.org/)