モダンフロントエンドアプリケーションの状態管理
Ethan Miller
Product Engineer · Leapcell

はじめに
進化し続けるフロントエンド開発の状況において、堅牢でスケーラブル、かつ保守性の高いユーザーインターフェースを構築するには、アプリケーションの状態を効果的に管理することが不可欠です。アプリケーションが複雑になるにつれて、データの整合性、予測可能な更新、直感的な開発者エクスペリエンスを確保するという課題も増大します。長年にわたり、数多くの状態管理ソリューションが登場し、それぞれがこれらの複雑な課題に取り組むための独自の G-アプローチを提供してきました。この記事では、Reactエコシステムにおける3つの著名な候補、Redux Toolkit、Zustand、Jotai を深く掘り下げます。それぞれの特徴的なパラダイムを探り、それらの根本的なメカニズムを理解し、長所と短所を特定することで、最終的に次のプロジェクトに最も適したソリューションを選択するための知識を身につけます。
フロントエンドの状態管理におけるコアコンセプト
各ライブラリの詳細に入る前に、議論の基礎となるいくつかの基本的な概念について共通の理解を確立しましょう。
- 状態(State): アプリケーションのUIとロジックを駆動するデータ。ユーザー入力、取得されたデータ、UI設定などが含まれます。
- 状態管理(State Management): アプリケーションの状態を予測可能かつ効率的な方法で整理、保存、更新するプロセス。
- 集中型 vs 分散型状態(Centralized vs. Decentralized State):
- 集中型: すべてのアプリケーション状態は、どこからでもアクセス可能な単一のグローバルストアに格納されます。これは、単一の真実の源(Single Source of Truth)につながることがよくあります。
- 分散型: 状態は、さまざまなコンポーネントや、より小さく独立したストアに分散されます。
- イミュータビリティ(Immutability): 状態を直接変更しないという原則。代わりに、望ましい変更を含む新しい状態オブジェクトが作成されます。これにより、予期しない副作用を防ぎ、状態の変更を追跡およびデバッグしやすくすることができます。
- アクション/イベント(Actions/Events): 状態を変更する意図を記述するオブジェクトまたは関数。
- リデューサー(Reducers): 現在の状態とアクションを入力として受け取り、新しい状態を返す純粋関数。多くの集中型ソリューションでは、状態を変更する唯一のメカニズムです。
- セレクター(Selectors): グローバル状態から特定のデータ部分を抽出する関数。多くの場合、不要な再レンダリングを防ぐための最適化に使用されます。
- フック(Hooks): React 16.8で導入されたフックにより、関数コンポーネントは状態と副作用を管理できるようになり、コンポーネント内の状態管理がよりアクセスしやすく、 composable になりました。
Redux Toolkit 包括的な集中型ソリューション
Redux Toolkit(RTK)は、効率的なRedux開発のための公式な意見表明型(opinionated)ソリューションです。一般的なReduxパターンを簡略化し、boilerplateを削減し、より良い開発者エクスペリエンスを提供するために作成されました。RTKは、集中型でイミュータブルな状態管理アプローチを採用し、強力な開発者ツールと予測可能な状態フローを提供します。
Redux Toolkit の原則:
RTKはReduxのコア原則に基づいています。単一の真実の源(Reduxストア)、純粋なreducer関数を介した状態変更のみ、および発生したことを記述するアクション。RTKの主な機能は次のとおりです。
configureStore: 妥当なデフォルト値でストアのセットアップを簡略化します。createSlice: 特定の状態スライスのアクションとreducerの作成を自動化し、boilerplateを大幅に削減します。createAsyncThunk: API呼び出しなどの非同期ロジックの処理を合理化します。createSelector: パフォーマンスを最適化するためにセレクター関数をメモ化します。
実例: カウンターとtodoリストの管理。
// store.js import { configureStore, createSlice } from '@reduxjs/toolkit'; const counterSlice = createSlice({ name: 'counter', initialState: { value: 0 }, reducers: { increment: (state) => { state.value += 1; }, decrement: (state) => { state.value -= 1; }, incrementByAmount: (state, action) => { state.value += action.payload; }, }, }); const todosSlice = createSlice({ name: 'todos', initialState: [], reducers: { addTodo: (state, action) => { state.push({ id: Date.now(), text: action.payload, completed: false }); }, toggleTodo: (state, action) => { const todo = state.find((t) => t.id === action.payload); if (todo) { todo.completed = !todo.completed; } }, }, }); export const { increment, decrement, incrementByAmount } = counterSlice.actions; export const { addTodo, toggleTodo } = todosSlice.actions; export const store = configureStore({ reducer: { counter: counterSlice.reducer, todos: todosSlice.reducer, }, });
// Counter.js (React Component) import React from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { increment, decrement, incrementByAmount } from './store'; function Counter() { const count = useSelector((state) => state.counter.value); const dispatch = useDispatch(); return ( <div> <h2>Counter: {count}</h2> <button onClick={() => dispatch(increment())}>Increment</button> <button onClick={() => dispatch(decrement())}>Decrement</button> <button onClick={() => dispatch(incrementByAmount(5))}>Increment by 5</button> </div> ); } export default Counter;
アプリケーションシナリオ: RTKは、多くの相互作用するコンポーネントがあり、予測可能な状態更新、デバッグ機能(例:Redux DevTools)、および単一の真実のソースを必要とする、大規模で複雑なアプリケーションに最適です。特に、保守性と長期的なスケーラビリティが重要なエンタープライズレベルのアプリケーションに適しています。
Zustand ミニマリストでパフォーマンスの高いアプローチ
Zustandは、ミニマリストでフックベース、かつ高いパフォーマンスを持つ状態管理アプローチで、新鮮な空気をもたらします。Reduxとは異なり、Zustandはreducerに依存せず、Immerのようなライブラリによるイミュータビリティ強制(もちろん、これらと連携することも可能ですが)にも依存しません。代わりに、シンプルで機能的なAPIを使用してストアを作成し、状態を直接変更しますが、再レンダリングが最適化されることを保証します。
Zustand の原則:
Zustandの哲学は、シンプルさと直接性にあります。React Hooksを活用してコンポーネントをストアに接続し、そのコアAPIは信じられないほど小さいです。
create関数: ストアを定義する主な方法です。状態と更新関数を含むオブジェクトを返す関数を受け取ります。- 直接変更: Zustandは、更新関数内で状態オブジェクトの直接変更を許可するため、多くの開発者にとって状態更新がより自然に感じられます。状態変更を浅く比較することで、最適化された再レンダリングを実現します。
- メモ化なしのセレクター: デフォルトでは、Zustandのセレクターは明示的なメモ化を必要としません。選択された状態のスライスが実際に変更された場合にのみ、コンポーネントは再レンダリングされます。
実例: Zustandで同じカウンターとtodoリストを構築する。
// useStore.js import { create } from 'zustand'; // Counter store const createCounterSlice = (set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), decrement: () => set((state) => ({ count: state.count - 1 })), incrementByAmount: (amount) => set((state) => ({ count: state.count + amount })), }); // Todos store const createTodosSlice = (set) => ({ todos: [], addTodo: (text) => set((state) => ({ todos: [...state.todos, { id: Date.now(), text, completed: false }], })), toggleTodo: (id) => set((state) => ({ todos: state.todos.map((todo) => todo.id === id ? { ...todo, completed: !todo.completed } : todo ), })), }); // スライスを結合(オプション、個別のストアも可能) export const useBoundStore = create((...a) => ({ ...createCounterSlice(...a), ...createTodosSlice(...a), })); // より良いモジュール性のため、個別のストアも可能 export const useCounterStore = create(createCounterSlice); export const useTodosStore = create(createTodosSlice);
// Counter.js (React Component) import React from 'react'; import { useCounterStore } from './useStore'; // または useBoundStore function Counter() { const count = useCounterStore((state) => state.count); const increment = useCounterStore((state) => state.increment); const decrement = useCounterStore((state) => state.decrement); const incrementByAmount = useCounterStore((state) => state.incrementByAmount); return ( <div> <h2>Counter: {count}</h2> <button onClick={increment}>Increment</button> <button onClick={decrement}>Decrement</button> <button onClick={() => incrementByAmount(5)}>Increment by 5</button> </div> ); } export default Counter;
アプリケーションシナリオ: Zustandは、小規模から中規模のアプリケーション、またはReduxのオーバーヘッドと構造化されたboilerplateなしで、パフォーマンスが高く軽量な状態ソリューションが必要なシナリオに最適です。特に、いくつかのコンポーネント間で共有する必要があるローカルコンポーネントの状態、サイドプロジェクト、またはuseStateからグローバルソリューションへの移行に最適です。そのシンプルさは、迅速なプロトタイピングにも優れています。
Jotai 原始的で強力なアプローチ
Jotaiは、Recoilに触発された、アトムベースのユニークな状態管理アプローチを採用しています。単一のグローバルストアではなく、Jotaiは「アトム」と呼ばれる小さく独立した状態の断片を定義できます。これらのアトムは、互いに結合および派生させることができ、非常にモジュラーで柔軟な状態グラフを作成します。Jotaiは、グローバルレベルでReactのuseStateに近い概念を採用しています。
Jotai の原則:
Jotaiは、状態の反応的な単位であるアトムの概念を中心に展開しています。
- アトム: 読み書き可能な状態の基本単位。アトムは任意の値を保持できます。
- 派生アトム: アトムは、他のアトムから値を派生させることができ、依存関係が変更されると自動的に更新される計算済みの状態を作成します。これは、複雑なセレクターを作成するのに強力です。
useAtomフック: Reactコンポーネント内からアトムと対話するための主要なフックです。useStateと同様に、アトムの値 setter 関数を返します。- 設計による分散: 状態は、単一のストアに集められるのではなく、多くの小さなアトムに分散されます。
実例: Jotaiでカウンターとtodoリストを実装する。
// atoms.js import { atom } from 'jotai'; // Counter atoms export const countAtom = atom(0); export const incrementAtom = atom( null, // setter (get, set) => set(countAtom, get(countAtom) + 1) ); export const decrementAtom = atom( null, (get, set) => set(countAtom, get(countAtom) - 1) ); export const incrementByAmountAtom = atom( null, (get, set, amount) => set(countAtom, get(countAtom) + amount) ); // Todos atoms export const todosAtom = atom([]); export const addTodoAtom = atom( null, (get, set, text) => set(todosAtom, [...get(todosAtom), { id: Date.now(), text, completed: false }]) ); export const toggleTodoAtom = atom( null, (get, set, id) => set( todosAtom, get(todosAtom).map((todo) => todo.id === id ? { ...todo, completed: !todo.completed } : todo ) ) );
// Counter.js (React Component) import React from 'react'; import { useAtom } from 'jotai'; import { countAtom, incrementAtom, decrementAtom, incrementByAmountAtom } from './atoms'; function Counter() { const [count] = useAtom(countAtom); const [, increment] = useAtom(incrementAtom); const [, decrement] = useAtom(decrementAtom); const [, incrementByAmount] = useAtom(incrementByAmountAtom); return ( <div> <h2>Counter: {count}</h2> <button onClick={increment}>Increment</button> <button onClick={decrement}>Decrement</button> <button onClick={() => incrementByAmount(5)}>Increment by 5</button> </div> ); } export default Counter;
アプリケーションシナリオ: Jotaiは、高度にモジュラーで粒度の細かい状態を持つアプリケーションに最適です。アプリケーションの特定の部分に局所的であるが、必要であればグローバルにアクセスできる必要があるUI状態にとって優れています。その「原始的」な性質は、ユニークな状態グラフの要件や、非常に細かいレベルでのレンダリングの最適化に非常に柔軟性があります。また、useStateのシンプルさを評価するがグローバルなソリューションが必要な場合や、reducerのような意見表明型パターンを避けたいプロジェクトにも適しています。
結論
Redux Toolkit、Zustand、Jotai は、それぞれReactの状態管理のための説得力のあるソリューションを提供しますが、それぞれ異なるニーズと好みに対応しています。Redux Toolkitは、デバッグ可能性と明確で集中化された状態フローを要求する大規模アプリケーションに理想的な、包括的で意見表明的、かつ高度に構造化されたフレームワークを提供します。ミニマリストなAPIと直接的な状態操作を備えたZustandは、小規模プロジェクトや、より意見表明的でないアプローチを好む人々に最適な、パフォーマンスが高く軽量な代替手段を提供します。アトムベースの分散モデルを活用したJotaiは、非常にモジュラーなアプリケーションと高度な状態導出に優れた、きめ細やかな制御と卓越した柔軟性をもたらします。最終的に最適な選択は、プロジェクトの規模、チームの習熟度、およびアプリケーションの状態の構造化と対話に関する特定の要件に依存します。

