Redux・Zustand・Jotaiの違い
Reactの状態管理ライブラリには、Redux、Zustand、Jotaiなどがあります。
どれも複数のコンポーネントで状態を共有するために利用できますが、状態をどのような単位で管理し、どのようなルールで更新するかが異なります。
今回はReduxを基準として、それぞれの違いを整理します。
Reduxは状態の更新ルールを明確にする
Reduxでは、アプリケーションの状態をStoreに集約し、Actionをdispatchすることで状態を更新します。
Component
↓
dispatch(Action)
↓
Reducer
↓
Store
↓
Component
Reduxでは、画面から直接Stateを書き換えるのではなく、
- 何が起きたのかをActionで表す
- ReducerがActionを受け取る
- Reducerが新しいStateを作る
という流れで状態を更新します。
現在のReduxでは、Redux Toolkitを利用する方法が公式に推奨されています。Redux ToolkitのcreateSliceを使うことで、Reducer、Action Type、Action Creatorをまとめて定義できます。
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
type CounterState = {
count: number
}
const initialState: CounterState = {
count: 0,
}
const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
increment: (state) => {
state.count += 1
},
add: (state, action: PayloadAction<number>) => {
state.count += action.payload
},
},
})
export const { increment, add } = counterSlice.actions
export default counterSlice.reducer
コンポーネントからは、Actionをdispatchします。
const count = useSelector((state: RootState) => state.counter.count)
const dispatch = useDispatch()
<button onClick={() => dispatch(increment())}>
増やす
</button>
Reduxの特徴は、単に状態を共有できることではありません。
状態がどのようなイベントによって、どのように変化したのかを明確にできることが大きな特徴です。
Redux DevToolsでは、dispatchされたActionと、それによって変化したStateを追跡できます。状態更新の経緯を確認しやすいため、複雑な業務ロジックや多人数での開発に向いています。
ZustandはReduxの構造をシンプルにした考え方
ZustandとReduxは、どちらも基本的にイミュータブルな状態モデルを採用しています。
一方で、ReduxがActionやReducerを通して状態を更新するのに対して、ZustandではStoreに定義した関数から直接状態を更新します。Zustand公式の比較ページでも、ReduxとZustandは概念的に似ていますが、Zustandでは通常ReactのProviderが不要である点が違いとして挙げられています。
Component
↓
StoreのAction
↓
set
↓
Store
import { create } from 'zustand'
type CounterStore = {
count: number
increment: () => void
add: (value: number) => void
}
export const useCounterStore = create<CounterStore>()((set) => ({
count: 0,
increment: () =>
set((state) => ({
count: state.count + 1,
})),
add: (value) =>
set((state) => ({
count: state.count + value,
})),
}))
コンポーネントからは、Storeに定義した関数を直接呼び出します。
const count = useCounterStore((state) => state.count)
const increment = useCounterStore((state) => state.increment)
<button onClick={increment}>
増やす
</button>
Reduxでは、
dispatch(increment())
とActionをdispatchします。
Zustandでは、
increment()
とStoreの関数を直接実行します。
つまりZustandは、Reduxにある次のような仕組みを簡略化した設計と考えられます。
Redux
Component
↓
dispatch
↓
Action
↓
Reducer
↓
State更新
Zustand
Component
↓
Storeの関数
↓
State更新
コード量が少なく、状態と更新処理を近い場所に定義できるため、小規模から中規模のアプリケーションでは導入しやすい構成です。
その一方で、Reduxほど強いルールがないため、Storeの分割方法やActionの命名ルールをチームで決めておかないと、Storeが肥大化しやすくなります。
ReduxとZustandはSelectorの考え方が近い
ReduxとZustandでは、コンポーネントが必要なStateだけをSelectorで取得します。
Reduxの場合は、useSelectorを使用します。
const count = useSelector(
(state: RootState) => state.counter.count,
)
Zustandでは、Store HookにSelectorを渡します。
const count = useCounterStore(
(state) => state.count,
)
Zustand公式の比較ページでは、ReduxとZustandのレンダリング最適化の考え方に大きな違いはなく、どちらもSelectorによって必要な状態を選択する方法が推奨されています。
Store全体を取得するのではなく、コンポーネントが利用する値だけを選択する点は共通しています。
Redux
Store
└── Selector
└── 必要なStateを取得
Zustand
Store
└── Selector
└── 必要なStateを取得
そのため、Reduxを理解している場合、ZustandのSelectorも比較的理解しやすいでしょう。
JotaiはStoreではなくAtomを組み合わせる
JotaiはReduxやZustandとは異なり、状態をAtomという小さな単位で管理します。
import { atom } from 'jotai'
export const countAtom = atom(0)
コンポーネントでは、useAtomなどを使ってAtomを読み書きします。
import { useAtom } from 'jotai'
import { countAtom } from './countAtom'
const [count, setCount] = useAtom(countAtom)
<button onClick={() => setCount((value) => value + 1)}>
増やす
</button>
Zustand公式の比較ページでは、ZustandとJotaiの大きな違いを次のように説明しています。
Zustand
→ 単一のStoreを作る
Jotai
→ 小さなAtomを組み合わせる
JotaiはAtom同士を組み合わせて、別の状態を作ることもできます。Atomの依存関係に基づいて、必要なコンポーネントだけが更新される設計です。
const priceAtom = atom(1000)
const quantityAtom = atom(2)
const totalPriceAtom = atom((get) => {
const price = get(priceAtom)
const quantity = get(quantityAtom)
return price * quantity
})
この例では、priceAtomまたはquantityAtomが変更されると、totalPriceAtomの値も更新されます。
Reduxでは、まず大きなStateツリーを設計します。
Store
├── user
├── products
├── cart
└── notifications
Jotaiでは、必要な状態をAtomとして定義し、それらを組み合わせます。
userAtom
productsAtom
cartAtom
├── cartItemCountAtom
└── cartTotalPriceAtom
ReduxがトップダウンにState全体を設計する考え方だとすると、Jotaiは小さな状態を積み上げていくボトムアップな設計です。
状態更新の考え方を比較する
3つのライブラリは、状態を更新するときの考え方が異なります。
Redux
Reduxでは、イベントをActionとして表現します。
dispatch(productSelected(productId))
コードを読むときは、次の順番で処理を追います。
dispatch
↓
Action
↓
Reducer
↓
State
「何が起きた結果、Stateが変化したのか」が明確です。
Zustand
Zustandでは、Storeに定義したActionを直接呼び出します。
selectProduct(productId)
コードを読むときは、次の流れになります。
StoreのAction
↓
set
↓
State
Reduxより流れが短く、処理を追いやすい構造です。
Jotai
Jotaiでは、対象となるAtomを直接更新します。
setSelectedProductId(productId)
コードを読むときは、AtomとAtomの依存関係を確認します。
Atom
↓
依存するAtom
↓
Component
Jotaiでは、アプリケーション全体のStoreよりも、どのAtomがどのAtomに依存しているかが重要になります。
設計時の違い
Reduxで設計する場合
Reduxでは最初に、アプリケーション全体の状態とイベントを整理します。
どの状態をグローバルに管理するか
↓
機能ごとにSliceを分ける
↓
どのActionで状態が変わるか決める
↓
Reducerを定義する
例えば、顧客管理画面では次のように設計できます。
store
├── authentication
├── customerSearch
├── selectedCustomer
└── notifications
ReduxではStateだけではなく、業務上のイベントも設計します。
customerSearchStarted
customerSearchSucceeded
customerSearchFailed
customerSelected
customerSelectionCleared
状態変化を業務イベントとして表現したい場合に向いています。
Zustandで設計する場合
Zustandでは、どの機能がどの状態と操作を持つかを考えます。
どの状態を共有するか
↓
どの機能単位でStoreを作るか
↓
Storeが持つ操作を定義する
customerSearchStore
├── keyword
├── searchConditions
├── selectedCustomerId
├── setKeyword
├── selectCustomer
└── reset
Stateと操作をひとまとまりにして設計しやすい点が特徴です。
ただし、次のような巨大Storeを作らないように注意が必要です。
appStore
├── authentication
├── customer
├── modal
├── notification
├── theme
├── search
└── form
機能や責務ごとにStoreを分割するルールが必要です。
Jotaiで設計する場合
Jotaiでは、状態をどこまで小さなAtomに分解するかを考えます。
必要な状態をAtomとして定義する
↓
Atom同士の依存関係を作る
↓
コンポーネントが必要なAtomを利用する
searchKeywordAtom
selectedPrefectureAtom
searchConditionsAtom
selectedCustomerIdAtom
さらに、既存のAtomから派生したAtomを作れます。
searchKeywordAtom
selectedPrefectureAtom
↓
searchConditionsAtom
状態同士の依存関係が多い画面や、細かな状態を必要なコンポーネントだけで共有したい場合に使いやすい設計です。
3つのライブラリを比較する
| 観点 | Redux Toolkit | Zustand | Jotai |
|---|---|---|---|
| 状態の単位 | 1つのStoreとSlice | Store | Atom |
| 更新方法 | Actionをdispatch | Storeの関数を呼ぶ | Atomを更新 |
| 更新ロジック | Reducer | set |
Atomのwrite処理 |
| 設計の方向 | State全体から分割 | 機能単位のStore | 小さなAtomを合成 |
| Provider | Reactでは基本的に必要 | 基本的に不要 | 状況により利用 |
| 再レンダリング制御 | Selector | Selector | Atomの依存関係 |
| コード量 | 比較的多い | 少ない | 少ない |
| ルール | 強い | 比較的自由 | 比較的自由 |
| 状態変更の追跡 | 得意 | 比較的シンプル | Atom単位 |
| 向いている規模 | 中規模から大規模 | 小規模から中規模 | 小規模から中規模 |
| チーム開発 | 統一しやすい | 独自ルールが必要 | Atom設計のルールが必要 |
どのライブラリを選ぶか
どの状態管理ライブラリが優れているかではなく、どのように状態を設計したいかで判断します。
Reduxを選ぶ場合
Reduxは、次のような場合に向いています。
- 状態更新のルールを統一したい
- 多人数で開発する
- 業務ロジックが複雑
- Actionの履歴から状態変化を追いたい
- 状態管理を一定の構造に揃えたい
- 大規模なアプリケーションを長期運用する
Reduxはコード量が増えやすい一方で、誰が書いても比較的同じ構造になりやすい点がメリットです。
Zustandを選ぶ場合
Zustandは、次のような場合に向いています。
- Reduxほど厳密な構造は必要ない
- シンプルにStoreを作りたい
- Stateと操作を機能単位でまとめたい
- 少ないコードで実装したい
- Reduxに近いSelectorベースの設計を使いたい
Reduxの考え方を保ちながら、ActionやReducerの仕組みを簡略化したい場合に選びやすいライブラリです。
Jotaiを選ぶ場合
Jotaiは、次のような場合に向いています。
- 状態を小さな単位で管理したい
- 状態同士を組み合わせたい
- コンポーネントごとに必要な状態だけ購読したい
- 大きなStoreを最初に設計したくない
- Reactの
useStateに近い感覚で状態を共有したい
画面内に細かな状態が多く、それぞれの状態に依存関係がある場合に使いやすいライブラリです。
まとめ
Redux、Zustand、Jotaiの違いを簡単に表すと、次のようになります。
Redux
状態の変更をActionとReducerで管理する
Zustand
機能ごとのStoreに状態と操作をまとめる
Jotai
小さなAtomを組み合わせて状態を作る
Reduxは、状態変更の流れを明確にし、アプリケーション全体に統一したルールを持たせる設計です。
ZustandはReduxと似たイミュータブルな状態モデルとSelectorの考え方を持ちながら、ActionやReducerを必須とせず、より直接的に状態を更新します。
Jotaiは、単一のStoreを中心に考えるのではなく、小さなAtomとその依存関係を組み合わせて状態を設計します。
設計するときは、次のように考えると整理しやすくなります。
状態更新のルールと履歴を重視する
→ Redux
シンプルなStoreと少ないコードを重視する
→ Zustand
小さな状態の組み合わせを重視する
→ Jotai
Reduxはルールを先に設計し、Zustandは機能単位のStoreを設計し、Jotaiは状態同士の依存関係を設計します。
この設計思想の違いを理解することが、ライブラリを選ぶうえで最も重要です。