ZustandでRedux likeに状態管理する

2024/09/08に公開

はじめに

Reactで状態管理と聞くと、Reduxを思い浮かべる人は多いでしょう。強力で拡張性が高い一方、案件によってはそこまで複雑にしなくてもいいのになと感じたことはあるはず。今回はReduxのように信頼性を保ちながら、より直感的で軽量なZustandを使った状態管理の方法を紹介していきます。

Zustandの基本形

まず、Zustandでの基本の書き方は以下のように記述します。

import create from 'zustand'

// 状態の型定義
interface CounterState {
  count: number;
  increase: () => void;
  decrease: () => void;
}

// Zustandのストアを作成
const useCounterStore = create<CounterState>((set) => ({
  count: 0, // 初期状態
  increase: () => set((state) => ({ count: state.count + 1 })), // カウントを増やす
  decrease: () => set((state) => ({ count: state.count - 1 })), // カウントを減らす
}));

// ストアを使用するコンポーネント
const CounterComponent = () => {
  const { count, increase, decrease } = useCounterStore();

  return (
    <div>
      <h1>Count: {count}</h1>
      <button onClick={increase}>Increase</button>
      <button onClick={decrease}>Decrease</button>
    </div>
  );
};

export default CounterComponent;

ポイント

  1. interface CounterState: 状態とその操作の型を定義しています。この例では、カウントの値とそれを増減させる関数が含まれています。
  2. useCounterStore: Zustandのcreate関数を使用して、状態管理のためのストアを作成します。
  3. increase / decrease: set関数を使って、現在の状態を元に新しい状態を設定します。
  4. CounterComponent: Zustandのストアからcountとアクション(increase/decrease)を取得し、ボタンをクリックすることでカウントを増減させます。

Reduxとの違い

Reduxの場合、Action、Reducer、Storeを用意する必要があり、Fluxパターンで処理の流れを記述しますが、ZustandはStoreを用意してその中にファンクションを記述していくので非常にシンプルに書くことができます。また、シンプルな設計なのでパフォーマンスにも優れており、デフォルトで必要なコンポーネントのみ再レンダリングされる仕組みになっています。

ZustandでRedux-likeに記述する

こういったZustandのシンプルかつ柔軟で高パフォーマンスであるというメリットを享受しつつ、軽量なアプリだけでなく、規模の大きなアプリケーションでも対応できるようにしたいときは以下のようにRedux-likeな書き方をする方法があります。

const types = { increase: 'INCREASE', decrease: 'DECREASE' }

const reducer = (state, { type, by = 1 }) => {
  switch (type) {
    case types.increase:
      return { grumpiness: state.grumpiness + by }
    case types.decrease:
      return { grumpiness: state.grumpiness - by }
  }
}

const useGrumpyStore = create((set) => ({
  grumpiness: 0,
  dispatch: (args) => set((state) => reducer(state, args)),
}))

const dispatch = useGrumpyStore((state) => state.dispatch)
dispatch({ type: types.increase, by: 2 })

ポイント

  1. アクションタイプの定義: CounterActionとして、Reduxと同様にアクションの型を定義しています。この例では、INCREASE, DECREASE, RESETの3つのアクションを定義しています。
  2. リデューサー関数: counterReducerは、Reduxのリデューサーと同じように、現在の状態とアクションを受け取って新しい状態を返す関数です。
  3. Zustandストアの作成: create関数を使い、dispatchメソッドを用意しています。dispatchはアクションを受け取り、リデューサーに渡して状態を更新します。
  4. Reactコンポーネントでの使用: dispatchメソッドを使って、ボタンのクリックに応じたアクションをディスパッチし、状態を更新します。

まとめ

Zustandを採用しつつも将来的にアプリケーションの規模が大きくなることを念頭に、Redux-likeに書くことはこのようなメリットがあります。

  • コードの保守性、可読性の担保
    • コードが複雑になるにつれて、ファンクションや値が多くなることでコードが煩雑になりがちですが、このRedux-likeな書き方をAction,Reducer,storeでファイルを分割することで、コードの管理が容易になります。
  • 高パフォーマンスかつ柔軟
    • Zustandはシンプルなので高パフォーマンスでありつつ、柔軟に記述することができます。
  • Reduxの知識を使える
    • Reduxになれている開発者にとっても、キャッチアップが容易になります。

筆者としては、Zustandはこのような書き方をすることで小規模なアプリケーションだけでなく大規模なものにも十分に対応することができると考えています。他にもZustandを使った良い方法などがあればコメントをもらえると嬉しいです。

Discussion