Zustand完全ガイド
Zustand完全ガイド
Section titled “Zustand完全ガイド”Zustandを使用した状態管理を、実務で使える実装例とベストプラクティスとともに詳しく解説します。
1. Zustandとは
Section titled “1. Zustandとは”Zustandの特徴
Section titled “Zustandの特徴”Zustandは、シンプルでミニマリストな状態管理ライブラリです。Reduxの代替として、より少ないボイラープレートで状態管理を実現します。
Zustandの特徴 ├─ シンプルなAPI ├─ 少ないボイラープレート ├─ 高いパフォーマンス └─ TypeScriptサポートなぜZustandが必要か
Section titled “なぜZustandが必要か”問題のある構成(Redux):
// 問題: ボイラープレートが多いimport { configureStore, createSlice } from '@reduxjs/toolkit';import { Provider } from 'react-redux';import { useSelector, useDispatch } from 'react-redux';
// Storeの作成const counterSlice = createSlice({ name: 'counter', initialState: { value: 0 }, reducers: { increment: (state) => { state.value += 1; }, },});
export const store = configureStore({ reducer: { counter: counterSlice.reducer },});
// Providerの設定function App() { return ( <Provider store={store}> <YourApp /> </Provider> );}
// 使用function Counter() { const count = useSelector((state) => state.counter.value); const dispatch = useDispatch(); return <button onClick={() => dispatch(counterSlice.actions.increment())}>+</button>;}解決: Zustandによる簡潔な実装
// 解決: Zustandによる簡潔な実装import { create } from 'zustand';
const useStore = create((set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })),}));
// 使用function Counter() { const count = useStore((state) => state.count); const increment = useStore((state) => state.increment); return <button onClick={increment}>+</button>;}2. インストール
Section titled “2. インストール”基本的なインストール
Section titled “基本的なインストール”npm install zustandTypeScriptでの使用
Section titled “TypeScriptでの使用”npm install zustand# TypeScriptは標準でサポートされている3. Storeの作成
Section titled “3. Storeの作成”基本的なStore
Section titled “基本的なStore”import { create } from 'zustand';
const useCounterStore = create((set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), decrement: () => set((state) => ({ count: state.count - 1 })), reset: () => set({ count: 0 }),}));
export default useCounterStore;TypeScriptでのStore
Section titled “TypeScriptでのStore”import { create } from 'zustand';
interface CounterState { count: number; increment: () => void; decrement: () => void; reset: () => void;}
const useCounterStore = create<CounterState>((set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), decrement: () => set((state) => ({ count: state.count - 1 })), reset: () => set({ count: 0 }),}));
export default useCounterStore;複雑なStore
Section titled “複雑なStore”import { create } from 'zustand';
const useUserStore = create((set) => ({ user: null, loading: false, error: null, setUser: (user) => set({ user }), setLoading: (loading) => set({ loading }), setError: (error) => set({ error }), fetchUser: async (userId) => { set({ loading: true, error: null }); try { const response = await fetch(`/api/users/${userId}`); const user = await response.json(); set({ user, loading: false }); } catch (error) { set({ error: error.message, loading: false }); } },}));
export default useUserStore;4. コンポーネントでの使用
Section titled “4. コンポーネントでの使用”基本的な使用
Section titled “基本的な使用”import useCounterStore from '../store/counterStore';
function Counter() { const count = useCounterStore((state) => state.count); const increment = useCounterStore((state) => state.increment); const decrement = useCounterStore((state) => state.decrement);
return ( <div> <p>Count: {count}</p> <button onClick={increment}>+</button> <button onClick={decrement}>-</button> </div> );}細かい粒度での選択
Section titled “細かい粒度での選択”// パフォーマンス最適化: 必要な部分だけを選択function Counter() { // countが変更されたときだけ再レンダリング const count = useCounterStore((state) => state.count); return <div>{count}</div>;}
function IncrementButton() { // incrementが変更されたときだけ再レンダリング(実際には変更されない) const increment = useCounterStore((state) => state.increment); return <button onClick={increment}>+</button>;}複数の値を選択
Section titled “複数の値を選択”// 複数の値を一度に選択function UserProfile() { const { user, loading, error } = useUserStore((state) => ({ user: state.user, loading: state.loading, error: state.error, }));
if (loading) return <div>Loading...</div>; if (error) return <div>Error: {error}</div>; return <div>{user?.name}</div>;}5. ミドルウェア
Section titled “5. ミドルウェア”DevTools
Section titled “DevTools”import { create } from 'zustand';import { devtools } from 'zustand/middleware';
const useCounterStore = create( devtools( (set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), }), { name: 'CounterStore' } ));Persist(永続化)
Section titled “Persist(永続化)”import { create } from 'zustand';import { persist } from 'zustand/middleware';
const useUserStore = create( persist( (set) => ({ user: null, setUser: (user) => set({ user }), }), { name: 'user-storage', // localStorageのキー名 } ));ミドルウェアの組み合わせ
Section titled “ミドルウェアの組み合わせ”import { create } from 'zustand';import { devtools, persist } from 'zustand/middleware';
const useCounterStore = create( devtools( persist( (set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), }), { name: 'counter-storage' } ), { name: 'CounterStore' } ));6. 実務でのベストプラクティス
Section titled “6. 実務でのベストプラクティス”パターン1: Storeの分割
Section titled “パターン1: Storeの分割”import { create } from 'zustand';
const useUserStore = create((set) => ({ user: null, setUser: (user) => set({ user }),}));
// store/themeStore.jsimport { create } from 'zustand';
const useThemeStore = create((set) => ({ theme: 'light', setTheme: (theme) => set({ theme }),}));
// 使用function App() { const user = useUserStore((state) => state.user); const theme = useThemeStore((state) => state.theme); return <div className={theme}>{user?.name}</div>;}パターン2: カスタムフックの作成
Section titled “パターン2: カスタムフックの作成”import useUserStore from '../store/userStore';
export function useUser() { const user = useUserStore((state) => state.user); const setUser = useUserStore((state) => state.setUser); const fetchUser = useUserStore((state) => state.fetchUser);
return { user, setUser, fetchUser };}
// 使用function UserProfile() { const { user, fetchUser } = useUser(); useEffect(() => { fetchUser(userId); }, [userId]); return <div>{user?.name}</div>;}パターン3: 非同期処理
Section titled “パターン3: 非同期処理”import { create } from 'zustand';
const useUserStore = create((set, get) => ({ user: null, loading: false, error: null, fetchUser: async (userId) => { set({ loading: true, error: null }); try { const response = await fetch(`/api/users/${userId}`); const user = await response.json(); set({ user, loading: false }); } catch (error) { set({ error: error.message, loading: false }); } }, updateUser: async (userId, data) => { set({ loading: true }); try { const response = await fetch(`/api/users/${userId}`, { method: 'PUT', body: JSON.stringify(data), }); const user = await response.json(); set({ user, loading: false }); } catch (error) { set({ error: error.message, loading: false }); } },}));7. よくある問題と解決策
Section titled “7. よくある問題と解決策”問題1: 不要な再レンダリング
Section titled “問題1: 不要な再レンダリング”原因:
- オブジェクト全体を選択している
- 浅い比較が機能していない
解決策:
// 問題: オブジェクト全体を選択const { user, loading } = useUserStore((state) => ({ user: state.user, loading: state.loading,}));
// 解決: 個別に選択const user = useUserStore((state) => state.user);const loading = useUserStore((state) => state.loading);
// または、shallowを使用import { shallow } from 'zustand/shallow';
const { user, loading } = useUserStore( (state) => ({ user: state.user, loading: state.loading }), shallow);問題2: 型安全性の問題
Section titled “問題2: 型安全性の問題”原因:
- TypeScriptの型定義が不十分
解決策:
// 適切な型定義interface UserState { user: User | null; setUser: (user: User) => void;}
const useUserStore = create<UserState>((set) => ({ user: null, setUser: (user) => set({ user }),}));これで、Zustandの基礎知識と実務での使い方を理解できるようになりました。