Zustand のレガシーな状態管理からの移行ステップ

React アプリケーションの状態管理は、開発者にとって常に大きな課題です。Redux、Context API、MobX など、様々なライブラリが存在する中で、Zustand はそのシンプルさとパフォーマンスの高さから注目を集めています。
しかし、既存のプロジェクトでレガシーな状態管理を使用している場合、移行は決して簡単ではありません。この記事では、実際のプロジェクトで直面する課題と、段階的に Zustand に移行するための具体的な手順をご紹介します。
レガシー状態管理の課題と Zustand の利点
レガシー状態管理が抱える問題
多くの React プロジェクトでは、以下のような状態管理パターンが使用されています:
Redux の場合:
- ボイラープレートコードが多すぎる
- 学習コストが高い
- 小さな状態変更でも多くのファイルを編集する必要がある
Context API の場合:
- パフォーマンスの問題(不要な再レンダリング)
- Provider 地獄の発生
- 型安全性の欠如
MobX の場合:
- 複雑な概念(observable、action、computed)
- デバッグの困難さ
- バンドルサイズの肥大化
Zustand が解決する課題
Zustand は、これらの問題を以下のように解決します:
typescript// 従来のReduxの場合
// actions.js
export const increment = () => ({
type:INCREMENT'
});
// reducer.js
const counterReducer = (state =0ion) => {
switch (action.type) {
case INCREMENT':
return state + 1;
default:
return state;
}
};
// コンポーネント
const mapStateToProps = (state) => ({
count: state.counter
});
const mapDispatchToProps = { increment };
export default connect(mapStateToProps, mapDispatchToProps)(Counter);
typescript// Zustandの場合
import { create } fromzustand';
const useCounterStore = create((set) => ({
count: 0 increment: () => set((state) => ({ count: state.count + 1 })),
}));
// コンポーネント
const Counter = () => {
const [object Object]count, increment } = useCounterStore();
return <button onClick={increment}>{count}</button>;
};
この違いを見ると、Zustand がいかにシンプルで直感的かがわかります。ボイラープレートコードが大幅に削減され、開発効率が向上します。
移行前の準備と環境構築
現在の状態管理の把握
移行を始める前に、現在のプロジェクトの状態管理を詳しく分析する必要があります。
bash# プロジェクトの依存関係を確認
yarn list | grep -E "(redux|mobx|context)
# 状態管理関連のファイルを検索
find src -name*.js" -o -name*.ts-o -name "*.tsx" | xargs grep -l "useState\|useReducer\|useContext\|connect\|Provider
Zustand のインストール
bash# Zustandのインストール
yarn add zustand
# TypeScriptを使用している場合
yarn add -D @types/zustand
開発ツールの設定
Zustand DevTools を設定することで、状態の変化を可視化できます:
typescriptimport { create } fromzustand';
import[object Object] devtools } from 'zustand/middleware;
const useStore = create(
devtools(
(set) => ([object Object] // ストアの定義
}),
[object Object] name: my-store', // DevToolsで表示される名前
}
)
);
段階的移行の実践手順
ステップ 1 既存状態の分析とマッピング
まず、現在の状態管理を詳しく分析し、Zustand ストアへの移行計画を立てます。
状態の可視化ツールの作成:
typescript// 既存の状態を分析するためのヘルパー関数
const analyzeExistingState = () => {
// Reduxの場合
const reduxState = store.getState();
console.log('Redux State Structure:', reduxState);
// Context APIの場合
const contextValues = document.querySelectorAll('data-context]');
console.log(Context Values:', contextValues);
// 状態の依存関係をマッピング
const stateDependencies = [object Object] user:auth, ile', 'preferences'],
cart: ['products,pricing'],
ui: ['theme,sidebar,modals']
};
return stateDependencies;
};
よくあるエラーと解決方法:
typescript// エラー: Cannot read property 'getStateof undefined
// 原因: Reduxストアが正しく初期化されていない
const store = createStore(reducer, initialState);
// エラー: Provider value is undefined
// 原因: Contextの値が正しく提供されていない
const MyProvider = ({ children }) =>[object Object] constvalue, setValue] = useState(initialValue);
return (
<MyContext.Provider value={{ value, setValue }}>
[object Object]children}
</MyContext.Provider>
);
};
ステップ 2: Zustand ストアの設計と実装
既存の状態を分析した結果に基づいて、Zustand ストアを設計します。
基本的なストア構造:
typescriptimport { create } fromzustand';
import { persist } from 'zustand/middleware;
// ユーザー関連のストア
interface UserState {
user: User | null;
isAuthenticated: boolean;
login: (credentials: LoginCredentials) => Promise<void>;
logout: () => void;
updateProfile: (profile: Partial<User>) => void;
}
const useUserStore = create<UserState>()(
persist(
(set, get) => ([object Object] user: null,
isAuthenticated: false,
login: async (credentials) => {
try[object Object] const user = await authService.login(credentials);
set({ user, isAuthenticated: true });
} catch (error) {
console.error('Login failed:', error);
throw error;
}
},
logout: () => {
set({ user: null, isAuthenticated: false });
},
updateProfile: (profile) => {
const currentUser = get().user;
if (currentUser)[object Object] set({ user: { ...currentUser, ...profile } });
}
},
}),
[object Object]
name: 'user-storage',
partialize: (state) => ([object Object] user: state.user, isAuthenticated: state.isAuthenticated }),
}
)
);
複雑な状態の分割:
typescript// 大きなストアを複数の小さなストアに分割
const useCartStore = create<CartState>()((set, get) => ({
items: [],
total: 0 addItem: (product) => {
const { items } = get();
const existingItem = items.find(item => item.id === product.id);
if (existingItem) {
set({
items: items.map(item =>
item.id === product.id
? { ...item, quantity: item.quantity +1 : item
)
});
} else {
set([object Object] items: [...items, { ...product, quantity: 1 }] });
}
// 合計を再計算
get().calculateTotal();
},
calculateTotal: () => {
const { items } = get();
const total = items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
set({ total });
},
}));
ステップ 3 コンポーネントの段階的置き換え
既存のコンポーネントを段階的に Zustand に移行します。
Redux から Zustand への移行例:
typescript// 移行前(Redux)
import { connect } from 'react-redux';
import { increment, decrement } from './actions';
const Counter = ([object Object] count, increment, decrement }) => (
<div>
<span>{count}</span>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
);
const mapStateToProps = (state) => ({
count: state.counter.count
});
const mapDispatchToProps = { increment, decrement };
export default connect(mapStateToProps, mapDispatchToProps)(Counter);
typescript// 移行後(Zustand)
import[object Object] useCounterStore } from./stores/counterStore';
const Counter = () => {
const [object Object] count, increment, decrement } = useCounterStore();
return (
<div>
<span>{count}</span>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
);
};
export default Counter;
Context API から Zustand への移行例:
typescript// 移行前(Context API)
import { useContext } fromreact';
import { ThemeContext } from './ThemeContext';
const ThemedButton = () => {
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<button
onClick={toggleTheme}
className={`btn btn-${theme}`}
>
Toggle Theme
</button>
);
};
typescript// 移行後(Zustand)
import { useThemeStore } from './stores/themeStore';
const ThemedButton = () => {
const { theme, toggleTheme } = useThemeStore();
return (
<button
onClick={toggleTheme}
className={`btn btn-${theme}`}
>
Toggle Theme
</button>
);
};
よくある移行エラーと解決方法:
typescript// エラー: Cannot read properties of undefined (reading user)
// 原因: ストアの初期化が完了する前にアクセスしている
const UserProfile = () => {
const user = useUserStore((state) => state.user);
// 初期化チェックを追加
if (!user) {
return <div>Loading...</div>;
}
return <div>{user.name}</div>;
};
// エラー: Maximum update depth exceeded
// 原因: ストアの更新が無限ループを引き起こしている
const useCounterStore = create((set) => ({
count: 0 increment: () => set((state) => ({ count: state.count + 1/ 間違った例: increment: () => set({ count: get().count + 1 })
}));
ステップ 4 テストと検証
移行後の動作を確認し、問題がないことを検証します。
ストアのテスト:
typescriptimport[object Object] renderHook, act } from@testing-library/react';
import[object Object] useCounterStore } from./stores/counterStore';
describe('Counter Store', () =>[object Object] beforeEach(() => [object Object]
// 各テスト前にストアをリセット
useCounterStore.setState({ count: 0 });
});
test('should increment count', () => {
const { result } = renderHook(() => useCounterStore());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
test('should handle async operations', async () => {
const { result } = renderHook(() => useUserStore());
await act(async () => {
await result.current.login({ email:test@example.com', password: password' });
});
expect(result.current.isAuthenticated).toBe(true);
});
});
統合テスト:
typescriptimport { render, screen, fireEvent } from@testing-library/react';
import Counter from ./Counter';
test(Counter component works with Zustand store', () => {
render(<Counter />);
const countDisplay = screen.getByText('0');
const incrementButton = screen.getByText('+);
fireEvent.click(incrementButton);
expect(countDisplay).toHaveTextContent(1);
});
移行時の注意点とベストプラクティス
段階的移行の重要性
一度にすべてを移行しようとすると、予期しない問題が発生する可能性があります。以下の順序で進めることをお勧めします:
- 非クリティカルな機能から開始 2 依存関係の少ないコンポーネントを優先 3 テストカバレッジの高い部分から着手
型安全性の確保
TypeScript を使用している場合、型定義を適切に行うことが重要です:
typescript// ストアの型定義
interface AppState {
user: UserState;
cart: CartState;
ui: UIState;
}
// セレクターの型安全性
const useUser = () => useUserStore((state) => state.user);
const useCartItems = () =>
useCartStore((state) => state.items);
// 型安全なアクション
const useUserActions = () =>
useUserStore((state) => ({
login: state.login,
logout: state.logout,
updateProfile: state.updateProfile,
}));
パフォーマンスの最適化
Zustand はデフォルトで効率的ですが、さらなる最適化が可能です:
typescript// 不要な再レンダリングを防ぐ
const UserName = () => [object Object]
// 特定のプロパティのみを購読
const name = useUserStore((state) => state.user?.name);
return <span>{name}</span>;
};
// メモ化による最適化
const ExpensiveComponent = memo(() => {
const data = useDataStore((state) => state.expensiveData);
return <div>{/* 重い処理 */}</div>;
});
よくある問題と解決策
typescript// 問題: ストアの状態が期待通りに更新されない
// 解決策: immerを使用した不変性の確保
import { immer } from 'zustand/middleware/immer;
const useStore = create(
immer((set) => ({
items: addItem: (item) => set((state) => {
state.items.push(item);
}),
}))
);
// 問題: 非同期処理での状態管理
// 解決策: 適切なエラーハンドリング
const useAsyncStore = create((set) => ({
data: null,
loading: false,
error: null,
fetchData: async () => {
set({ loading: true, error: null });
try [object Object] const data = await api.getData();
set({ data, loading: false });
} catch (error) {
set({ error: error.message, loading: false });
}
},
}));
パフォーマンス最適化のポイント
不要な再レンダリングの防止
Zustand の最大の利点の一つは、必要な部分のみを購読できることです:
typescript// 効率的なセレクターの使用
const useOptimizedStore = () => [object Object]
// 複数の値を一度に取得
const { user, cart } = useStore((state) => ({
user: state.user,
cart: state.cart,
}));
// 特定の値のみを購読
const userName = useStore((state) => state.user?.name);
const cartCount = useStore((state) => state.cart.items.length);
return { user, cart, userName, cartCount };
};
メモリリークの防止
typescript// コンポーネントのアンマウント時にクリーンアップ
const useCleanupStore = create((set) => ([object Object]subscriptions: new Set(),
subscribe: (callback) => {
const subscription = { id: Date.now(), callback };
set((state) => ({
subscriptions: new Set([...state.subscriptions, subscription])
}));
// クリーンアップ関数を返す
return () => [object Object]
set((state) => {
const newSubscriptions = new Set(state.subscriptions);
newSubscriptions.delete(subscription);
return { subscriptions: newSubscriptions };
});
};
},
));
バンドルサイズの最適化
typescript// 必要な部分のみをインポート
import { create } fromzustand';
import { persist } from 'zustand/middleware';
// 開発時のみDevToolsを使用
const devTools = process.env.NODE_ENV === development'
? require('zustand/middleware).devtools
: (fn) => fn;
const useStore = create(
devTools(
persist(
(set) => ({
// ストアの定義
}),
[object Object]name: 'app-storage' }
)
)
);
まとめ
Zustand への移行は、最初は複雑に感じるかもしれません。しかし、段階的なアプローチを取ることで、リスクを最小限に抑えながら、より効率的で保守性の高い状態管理システムを構築できます。
移行の成功の鍵は、以下の点にあります:
1 十分な計画と分析 - 現在の状態管理を深く理解する 2. 段階的な実装 - 一度にすべてを変更しようとしない 3. 徹底的なテスト - 各段階で動作を確認する 4. チームの教育 - 新しいパターンに慣れる時間を設ける
Zustand のシンプルさとパフォーマンスの高さは、開発体験を大幅に向上させます。レガシーな状態管理からの移行は、短期的には労力を要しますが、長期的には確実に価値のある投資となるでしょう。
移行を成功させるためには、焦らずに一歩ずつ進めることが重要です。各ステップで学んだことを活かし、チーム全体で新しい状態管理パターンに慣れていくことで、より良いアプリケーションを構築できるようになります。
関連リンク
- Zustand 公式ドキュメント -Zustand DevTools](https://github.com/pmndrs/zustand/tree/main/src/middleware/devtools)
- React 状態管理の比較
- TypeScript と Zustand -Zustand ベストプラクティス](https://github.com/pmndrs/zustand#best-practices)
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来