ZustandとReduxは何が違う?Zustandの設計思想を理解する

昨今の React アプリケーション開発には、さまざまなステート管理ライブラリがあります。その中でも特に注目したいのが、長年のスタンダードだった Redux と、比較的新しく登場した Zustand の違いです。両者はどのような設計思想の違いがあるのでしょうか?この記事では、Redux と Zustand の根本的な哲学の違いに焦点を当てて解説します。
背景:Flux アーキテクチャからの進化
Flux アーキテクチャからの進化
状態管理の話をするとき、避けて通れないのが Facebook が提唱した Flux アーキテクチャです。Flux は、データが一方向にのみ流れるという考え方を導入し、React 開発における予測可能性を高めました。
sqlAction → Dispatcher → Store → View
↑ |
└───────────────────────────┘
この単方向データフローの概念は、React エコシステム全体に大きな影響を与えました。そして、Flux の考え方を洗練させたのが Redux です。
Redux の設計原則(三原則)とその意図
Redux は以下の三原則に基づいて設計されています:
-
単一の信頼できる情報源(Single source of truth)
- アプリケーションの状態は単一のストアに格納される
- これにより、デバッグやテストが容易になる
-
状態は読み取り専用(State is read-only)
- 状態を変更する唯一の方法はアクションをディスパッチすること
- これにより、変更が予測可能になる
-
変更はピュア関数で行う(Changes are made with pure functions)
- リデューサーは純粋関数であるべき
- 副作用を排除し、同じ入力に対して同じ出力を保証する
これらの原則の意図は明確です。アプリケーションの状態変化を予測可能で追跡可能にし、バグの発生を減らすことです。特に大規模なアプリケーションにおいて、この厳格な構造は非常に価値があります。
モダン React が求める新しいパラダイム
しかし、React そのものが進化するにつれて、状態管理に対する考え方も変わってきました。React Hooks の登場は特に大きな転換点でした。
jsx// React Hooks以前の状態管理
class Counter extends Component {
state = { count: 0 };
increment = () => {
this.setState((prevState) => ({
count: prevState.count + 1,
}));
};
render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={this.increment}>Increment</button>
</div>
);
}
}
// React Hooks以降
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
);
}
Hooks により、関数コンポーネントでも状態管理が容易になり、よりシンプルで直感的なコードが書けるようになりました。これに伴い、状態管理ライブラリにも「シンプルさ」と「直感的な API」が求められるようになったのです。
課題:Redux 特有の設計の制約
Redux 特有の設計の制約
Redux の厳格な設計原則は、大きな強みである一方で、制約ともなります。
-
ボイラープレートの多さ
- アクションタイプ、アクションクリエイター、リデューサーなど、多くのコードを書く必要がある
- 単純な状態変更にも多くのセットアップが必要
-
学習曲線の高さ
- Flux、純粋関数、イミュータビリティなど多くの概念を理解する必要がある
- 特に初心者にとっては敷居が高い
-
柔軟性の欠如
- 厳格な構造により、特定のパターンに従わなければならない
- 場合によっては過剰な抽象化となる
単一ストアの長所と短所
Redux の単一ストア原則も、両刃の剣です。
長所:
- アプリケーションの状態を一元管理できる
- 状態の関係性を包括的に把握できる
- タイムトラベルデバッギングなどの高度な機能が実現可能
短所:
- すべての状態を 1 つの場所に集約するため、ストアが肥大化しやすい
- コード分割やモジュール化が難しくなる
- チーム開発時に、同じストアに対して複数の開発者が変更を加えると競合が発生しやすい
ボイラープレート問題の根本原因
Redux のボイラープレートが多い理由は、その設計思想に由来します。
-
明示性の重視
- すべての状態変更を明示的に定義することで追跡可能性を高める
- しかし、これが冗長なコードにつながる
-
型安全性の確保
- 厳格な型システムにより、実行時エラーを減らす
- しかし、そのために多くの型定義が必要になる
-
純粋性の追求
- 副作用を排除し、テスト容易性を高める
- しかし、実世界のアプリケーションには副作用が不可欠
これらの問題に対処するため、Redux Toolkit などのソリューションが登場しましたが、根本的な設計思想は変わっていません。
解決策:Zustand の設計原則と Redux との哲学的違い
Zustand の設計原則と Redux との哲学的違い
Zustand は、Redux の良い部分を継承しつつも、異なる設計哲学を持っています。
-
シンプリシティの重視
- 最小限の API と学習コスト
- 直感的な使用感を優先
-
React Hooks との統合
- Hooks ファーストな設計
- コンポーネントとの自然な統合
-
プラグマティックなアプローチ
- 実用性を重視し、理論的純粋性よりも開発者体験を優先
- 必要なときだけ複雑さを取り入れる
プラグマティズム vs 純粋性
Redux と Zustand の大きな違いの一つは、「プラグマティズム(実用主義)」と「純粋性」のバランスです。
Redux:純粋性を重視
- 厳格な原則と構造
- 予測可能性と追跡可能性の最大化
- 理論的に美しいアーキテクチャ
Zustand:プラグマティズムを重視
- 実用的なシンプルさ
- 開発者体験の最適化
- 必要十分な機能提供
例えば、Redux では状態更新のたびにアクションをディスパッチする必要がありますが、Zustand では直接状態を更新できます。
typescript// Redux
dispatch(incrementCounter());
// Zustand
useStore.getState().increment();
// またはコンポーネント内で
const increment = useStore((state) => state.increment);
increment();
関数型プログラミングの考え方の違い
両者は関数型プログラミングの原則を取り入れていますが、その適用方法に違いがあります。
Redux:厳格な関数型アプローチ
- イミュータビリティの厳格な強制
- すべてのリデューサーは純粋関数
- 副作用は厳密に分離(redux-saga や redux-observable などのミドルウェアに委託)
Zustand:プラグマティックな関数型アプローチ
- イミュータビリティを推奨するが強制はしない
- 純粋でない関数も許容
- 副作用を自然に扱える柔軟性
Zustand では、例えば非同期処理をストア内に直接書くことができます:
typescript// Zustand
const useStore = create((set) => ({
users: [],
fetchUsers: async () => {
const response = await fetch('/api/users');
const users = await response.json();
set({ users });
},
}));
一方、Redux では通常、このような非同期処理はミドルウェアを通じて行います。
具体例:同じ機能の実装比較(Redux vs Zustand)
同じ機能の実装比較(Redux vs Zustand)
カウンターの例で、両者の実装の違いを見てみましょう。
Redux 実装:
typescript// アクションタイプ
const INCREMENT = 'counter/increment';
const DECREMENT = 'counter/decrement';
const RESET = 'counter/reset';
// アクションクリエイター
const increment = () => ({ type: INCREMENT });
const decrement = () => ({ type: DECREMENT });
const reset = () => ({ type: RESET });
// リデューサー
const initialState = { count: 0 };
function counterReducer(state = initialState, action) {
switch (action.type) {
case INCREMENT:
return { count: state.count + 1 };
case DECREMENT:
return { count: state.count - 1 };
case RESET:
return { count: 0 };
default:
return state;
}
}
// ストア
const store = createStore(counterReducer);
// Reactコンポーネントでの使用
function Counter() {
const count = useSelector((state) => state.count);
const dispatch = useDispatch();
return (
<div>
<p>Count: {count}</p>
<button onClick={() => dispatch(increment())}>
Increment
</button>
<button onClick={() => dispatch(decrement())}>
Decrement
</button>
<button onClick={() => dispatch(reset())}>
Reset
</button>
</div>
);
}
Zustand 実装:
typescriptimport { create } from 'zustand';
// ストアの作成
const useCountStore = create((set) => ({
count: 0,
increment: () =>
set((state) => ({ count: state.count + 1 })),
decrement: () =>
set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
}));
// Reactコンポーネントでの使用
function Counter() {
const { count, increment, decrement, reset } =
useCountStore();
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
<button onClick={reset}>Reset</button>
</div>
);
}
Zustand の実装はわずか数行で済み、ボイラープレートが大幅に削減されています。また、アクションとロジックが密接に結びついており、より直感的です。
ミドルウェア設計の考え方の違い
ミドルウェアについても、両者のアプローチは大きく異なります。
Redux のミドルウェア:
- 強力な拡張ポイントとして設計
- アクションのディスパッチと処理の間に挿入される
- redux-thunk、redux-saga、redux-observable など多様なエコシステム
typescript// Redux Thunkの例
const fetchUsers = () => async (dispatch) => {
dispatch({ type: 'FETCH_USERS_REQUEST' });
try {
const response = await fetch('/api/users');
const users = await response.json();
dispatch({
type: 'FETCH_USERS_SUCCESS',
payload: users,
});
} catch (error) {
dispatch({ type: 'FETCH_USERS_FAILURE', error });
}
};
Zustand のミドルウェア:
- より軽量で特定の機能に特化
- ストア作成時に適用される
- よりシンプルな API
typescript// Zustandのpersistミドルウェア例
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
const useStore = create(
persist(
(set) => ({
count: 0,
increment: () =>
set((state) => ({ count: state.count + 1 })),
}),
{ name: 'count-storage' }
)
);
Redux のミドルウェアはより汎用的で強力ですが、Zustand のミドルウェアはより特化型で使いやすいという違いがあります。
デバッグと開発者体験の違い
デバッグツールと開発者体験も大きく異なります。
Redux の開発者体験:
- Redux DevTools は非常に強力
- タイムトラベルデバッギング
- アクションの詳細な履歴
- 複雑なセットアップが必要な場合も
Zustand の開発者体験:
- よりシンプルなデバッグ体験
- Redux DevTools との統合も可能
- セットアップが最小限
- ストアの状態を直接検査可能
typescript// Zustandでのデバッグ設定
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
const useStore = create(
devtools((set) => ({
count: 0,
increment: () =>
set((state) => ({ count: state.count + 1 })),
}))
);
// デバッグ情報の確認
console.log(useStore.getState());
まとめ:適材適所の選択
適材適所:どのような場合にどちらを選ぶか
Redux が適している場合:
- 大規模で複雑なアプリケーション
- 複数のデータ間の関係性が複雑な場合
- チーム規模が大きく、厳格な構造が必要な場合
- 詳細なデバッグと監査証跡が必要な場合
- 既存の Redux エコシステムを活用したい場合
Zustand が適している場合:
- 中小規模のアプリケーション
- 迅速な開発が優先される場合
- ボイラープレートを最小限にしたい場合
- React Hooks との自然な統合が重要な場合
- シンプルさと直感的な API を求める場合
- Redux DevTools の機能の一部だけが必要な場合
プロジェクトの特性に応じて、適切なツールを選択することが重要です。また、一つのプロジェクト内で両方を使い分けることも可能です。
両ライブラリの今後の展望
Redux の展望:
- Redux Toolkit の進化による使いやすさの向上
- TypeScript との統合の強化
- エコシステムの成熟と安定性
Zustand の展望:
- さらなるシンプル化と最適化
- より多くのミドルウェアとプラグイン
- React の新機能との継続的な統合
両ライブラリとも、それぞれの強みを活かしながら進化を続けていくでしょう。Redux はエンタープライズレベルの安定性と予測可能性を、Zustand は開発者体験とシンプルさを追求していくと予想されます。