T-CREATOR

Zustand Middleware 活用術:ログ・永続化・開発支援を強化する

Zustand Middleware 活用術:ログ・永続化・開発支援を強化する

React アプリケーションの状態管理において、「シンプルで使いやすい」と評判の Zustand ですが、その真の力は豊富なミドルウェアエコシステムにあります。「状態の変更をログに出力したい」「ページをリロードしても状態を保持したい」「開発中のデバッグ効率を向上させたい」そんな開発現場のニーズに応えるのが、Zustand のミドルウェア機能なのです。

しかし、多くの開発者がミドルウェアの恩恵を十分に活用できていないのが現状ではないでしょうか。基本的な状態管理は使えても、「ログはコンソールに手動出力」「永続化は自分で localStorage を操作」「開発支援は手動でデバッグ」といった非効率な作業を続けていませんか?

本記事では、Zustand のミドルウェアを活用して開発効率を劇的に向上させる実践的な手法をお伝えします。

Zustand Middleware が解決する実際の開発課題

現代の React 開発では、状態管理だけでなく、その周辺機能への要求も高まっています。実際の開発現場で頻繁に遭遇する課題を見てみましょう。

開発効率の課題

typescript// よくある非効率なパターン
const useUserStore = create<UserState>((set, get) => ({
  users: [],
  loading: false,

  fetchUsers: async () => {
    console.log('ユーザー取得開始'); // 手動ログ
    set({ loading: true });

    try {
      const users = await api.fetchUsers();
      console.log('ユーザー取得成功:', users); // 手動ログ
      set({ users, loading: false });

      // 手動で localStorage に保存
      localStorage.setItem('users', JSON.stringify(users));
    } catch (error) {
      console.error('ユーザー取得エラー:', error); // 手動ログ
      set({ loading: false });
    }
  },
}));

このようなコードでは、ログ出力・永続化・エラーハンドリングがビジネスロジックと密結合してしまい、保守性が低下します。

既存の状態管理ライブラリとの比較優位性

Zustand のミドルウェアシステムは、他の状態管理ライブラリと比較して独特の優位性を持っています。

#特徴ZustandRedux ToolkitRecoilJotai
1ミドルウェア追加の簡単さ★★★★★★★★☆☆★★☆☆☆★★★☆☆
2TypeScript 対応★★★★★★★★★☆★★★☆☆★★★★☆
3バンドルサイズへの影響★★★★★★★☆☆☆★★★☆☆★★★★☆
4学習コストの低さ★★★★★★★☆☆☆★★★☆☆★★★★☆

Zustand の最大の魅力は、ミドルウェアの適用が関数の合成という直感的な方法で行えることです。

typescript// Zustand のミドルウェア適用例
const useStore = create<State>()(
  devtools(
    persist(
      subscribeWithSelector(
        immer((set) => ({
          // ストアの実装
        }))
      ),
      { name: 'my-store' }
    )
  )
);

ミドルウェアの基本概念と設計思想

Zustand のミドルウェアは、関数型プログラミングの原則に基づいた美しい設計となっています。その仕組みを理解することで、より効果的な活用が可能になります。

Zustand Middleware のアーキテクチャ

ミドルウェアは、ストア作成関数を受け取り、拡張された機能を持つ新しいストア作成関数を返す高階関数です。

typescript// ミドルウェアの基本的な型定義
type StateCreator<T> = (
  set: SetState<T>,
  get: GetState<T>,
  api: StoreApi<T>
) => T;

type Middleware<T> = (
  stateCreator: StateCreator<T>
) => StateCreator<T>;

// シンプルなログミドルウェアの例
const loggerMiddleware =
  <T>(stateCreator: StateCreator<T>): StateCreator<T> =>
  (set, get, api) => {
    const loggedSet: SetState<T> = (partial, replace) => {
      console.log('状態変更前:', get());
      set(partial, replace);
      console.log('状態変更後:', get());
    };

    return stateCreator(loggedSet, get, api);
  };

この設計により、複数のミドルウェアを組み合わせることで、強力で柔軟な状態管理システムを構築できます。

関数合成による拡張可能な設計

Zustand のミドルウェアは関数合成の原理に基づいており、数学的に美しい方法で機能を組み合わせることができます。

typescript// 関数合成による複数ミドルウェアの適用
const createEnhancedStore = <T>(
  stateCreator: StateCreator<T>
) =>
  create<T>()(
    devtools(
      persist(subscribeWithSelector(immer(stateCreator)), {
        name: 'enhanced-store',
      })
    )
  );

// 実際の使用例
interface AppState {
  count: number;
  user: User | null;
  increment: () => void;
  setUser: (user: User) => void;
}

const useAppStore = createEnhancedStore<AppState>(
  (set) => ({
    count: 0,
    user: null,

    increment: () =>
      set((state) => {
        state.count += 1;
      }),

    setUser: (user) =>
      set((state) => {
        state.user = user;
      }),
  })
);

TypeScript との相性の良さ

Zustand のミドルウェアは TypeScript との相性が非常に良く、型安全性を保ちながら強力な機能を提供します。

typescript// 型安全なミドルウェア定義
interface LoggingConfig {
  enabled: boolean;
  level: 'debug' | 'info' | 'warn' | 'error';
  formatter?: (state: any) => string;
}

const createTypedLogger =
  <T>(config: LoggingConfig) =>
  (stateCreator: StateCreator<T>): StateCreator<T> =>
  (set, get, api) => {
    if (!config.enabled) {
      return stateCreator(set, get, api);
    }

    const enhancedSet: SetState<T> = (partial, replace) => {
      const prevState = get();
      set(partial, replace);
      const nextState = get();

      const message = config.formatter
        ? config.formatter(nextState)
        : `State updated: ${JSON.stringify(nextState)}`;

      console[config.level](message);
    };

    return stateCreator(enhancedSet, get, api);
  };

// 型安全な使用例
const useTypedStore = create<AppState>()(
  createTypedLogger<AppState>({
    enabled: process.env.NODE_ENV === 'development',
    level: 'info',
    formatter: (state) =>
      `Count: ${state.count}, User: ${
        state.user?.name || 'None'
      }`,
  })((set) => ({
    count: 0,
    user: null,
    increment: () =>
      set((state) => ({
        ...state,
        count: state.count + 1,
      })),
    setUser: (user) => set((state) => ({ ...state, user })),
  }))
);

ログ機能の実装と活用

開発効率を向上させる第一歩は、適切なログ機能の実装です。Zustand のミドルウェアを活用することで、手動でのログ出力から解放され、体系的で有用なログシステムを構築できます。

Logger Middleware の実装パターン

基本的なロガーから、実用的な機能を備えた高度なロガーまで、段階的に実装していきましょう。

基本的なロガーミドルウェア

typescriptinterface LogEntry {
  timestamp: Date;
  action: string;
  prevState: any;
  nextState: any;
  diff?: any;
}

const basicLogger =
  <T>(stateCreator: StateCreator<T>): StateCreator<T> =>
  (set, get, api) => {
    const logs: LogEntry[] = [];

    const loggedSet: SetState<T> = (
      partial,
      replace,
      action
    ) => {
      const prevState = get();
      set(partial, replace);
      const nextState = get();

      const entry: LogEntry = {
        timestamp: new Date(),
        action: action || 'unknown',
        prevState: structuredClone(prevState),
        nextState: structuredClone(nextState),
      };

      logs.push(entry);
      console.group(`🔄 State Change: ${entry.action}`);
      console.log('Previous:', entry.prevState);
      console.log('Next:', entry.nextState);
      console.log('Time:', entry.timestamp.toISOString());
      console.groupEnd();
    };

    // ログ取得関数をAPIに追加
    const enhancedApi = {
      ...api,
      getLogs: () => [...logs],
      clearLogs: () => logs.splice(0, logs.length),
    };

    return stateCreator(loggedSet, get, enhancedApi);
  };

高度な機能を持つロガーミドルウェア

typescriptinterface AdvancedLoggerConfig {
  enabled: boolean;
  level: 'debug' | 'info' | 'warn' | 'error';
  maxEntries: number;
  includeDiff: boolean;
  includeTrace: boolean;
  filter?: (action: string, state: any) => boolean;
  transform?: (state: any) => any;
}

const advancedLogger =
  <T>(
    config: AdvancedLoggerConfig = {
      enabled: true,
      level: 'info',
      maxEntries: 100,
      includeDiff: true,
      includeTrace: false,
    }
  ) =>
  (stateCreator: StateCreator<T>): StateCreator<T> =>
  (set, get, api) => {
    if (!config.enabled) {
      return stateCreator(set, get, api);
    }

    const logs: LogEntry[] = [];

    // 状態の差分を計算する関数
    const calculateDiff = (prev: any, next: any): any => {
      if (!config.includeDiff) return undefined;

      const diff: any = {};
      const allKeys = new Set([
        ...Object.keys(prev),
        ...Object.keys(next),
      ]);

      for (const key of allKeys) {
        if (prev[key] !== next[key]) {
          diff[key] = {
            from: prev[key],
            to: next[key],
          };
        }
      }

      return Object.keys(diff).length > 0
        ? diff
        : undefined;
    };

    const enhancedSet: SetState<T> = (
      partial,
      replace,
      action = 'unknown'
    ) => {
      const prevState = get();

      // フィルター適用
      if (
        config.filter &&
        !config.filter(action, prevState)
      ) {
        set(partial, replace);
        return;
      }

      set(partial, replace);
      const nextState = get();

      // 状態変換
      const transformedPrev = config.transform
        ? config.transform(prevState)
        : prevState;
      const transformedNext = config.transform
        ? config.transform(nextState)
        : nextState;

      const entry: LogEntry = {
        timestamp: new Date(),
        action,
        prevState: transformedPrev,
        nextState: transformedNext,
        diff: calculateDiff(
          transformedPrev,
          transformedNext
        ),
      };

      // ログエントリーの追加(最大数制限あり)
      logs.push(entry);
      if (logs.length > config.maxEntries) {
        logs.shift();
      }

      // コンソール出力
      const style = {
        debug: 'color: #8B5CF6',
        info: 'color: #3B82F6',
        warn: 'color: #F59E0B',
        error: 'color: #EF4444',
      };

      console.group(`%c🔄 ${action}`, style[config.level]);
      console.log(
        'Time:',
        entry.timestamp.toLocaleTimeString()
      );
      console.log('Previous:', entry.prevState);
      console.log('Next:', entry.nextState);

      if (entry.diff) {
        console.log('Diff:', entry.diff);
      }

      if (config.includeTrace) {
        console.trace('Call stack');
      }

      console.groupEnd();
    };

    return stateCreator(enhancedSet, get, {
      ...api,
      getLogs: () => [...logs],
      clearLogs: () => logs.splice(0, logs.length),
      exportLogs: () => JSON.stringify(logs, null, 2),
    });
  };

開発環境と本番環境の使い分け

環境に応じたログ設定の最適化は、パフォーマンスとデバッグ効率の両立に重要です。

typescript// 環境別ログ設定
const createEnvironmentAwareLogger = <T>() => {
  const isDevelopment =
    process.env.NODE_ENV === 'development';
  const isProduction =
    process.env.NODE_ENV === 'production';

  // 開発環境設定
  const developmentConfig: AdvancedLoggerConfig = {
    enabled: true,
    level: 'debug',
    maxEntries: 1000,
    includeDiff: true,
    includeTrace: true,
    filter: (action, state) =>
      !action.startsWith('_internal'),
    transform: (state) => state, // 変換なし
  };

  // 本番環境設定(エラーのみ)
  const productionConfig: AdvancedLoggerConfig = {
    enabled: true,
    level: 'error',
    maxEntries: 50,
    includeDiff: false,
    includeTrace: false,
    filter: (action, state) =>
      action.includes('error') || action.includes('failed'),
    transform: (state) => {
      // 本番環境では機密情報を除外
      const { password, token, ...safeState } =
        state as any;
      return safeState;
    },
  };

  // テスト環境設定
  const testConfig: AdvancedLoggerConfig = {
    enabled: false, // テスト中はログ無効
    level: 'warn',
    maxEntries: 10,
    includeDiff: false,
    includeTrace: false,
  };

  const config = isDevelopment
    ? developmentConfig
    : isProduction
    ? productionConfig
    : testConfig;

  return advancedLogger<T>(config);
};

// 使用例
const useUserStore = create<UserState>()(
  createEnvironmentAwareLogger<UserState>()((set, get) => ({
    users: [],
    loading: false,
    error: null,

    fetchUsers: async () => {
      set(
        { loading: true, error: null },
        false,
        'fetchUsers/start'
      );

      try {
        const users = await userApi.getUsers();
        set(
          { users, loading: false },
          false,
          'fetchUsers/success'
        );
      } catch (error) {
        set(
          {
            loading: false,
            error:
              error instanceof Error
                ? error.message
                : 'Unknown error',
          },
          false,
          'fetchUsers/error'
        );
      }
    },
  }))
);

パフォーマンスを考慮したログ設計

大量の状態変更が発生する場合、ログ処理がパフォーマンスボトルネックになる可能性があります。

typescript// パフォーマンス重視のロガー
const performantLogger =
  <T>(config: {
    batchSize: number;
    flushInterval: number;
    asyncLogging: boolean;
  }) =>
  (stateCreator: StateCreator<T>): StateCreator<T> =>
  (set, get, api) => {
    const logBuffer: LogEntry[] = [];
    let flushTimer: NodeJS.Timeout | null = null;

    // バッファをフラッシュする関数
    const flushLogs = async () => {
      if (logBuffer.length === 0) return;

      const logsToFlush = [...logBuffer];
      logBuffer.splice(0, logBuffer.length);

      if (config.asyncLogging) {
        // Web Worker または setTimeout で非同期処理
        setTimeout(() => {
          console.group('📦 Batched Logs');
          logsToFlush.forEach((entry) => {
            console.log(
              `${entry.action}:`,
              entry.nextState
            );
          });
          console.groupEnd();
        }, 0);
      } else {
        // 同期処理
        console.group('📦 Batched Logs');
        logsToFlush.forEach((entry) => {
          console.log(`${entry.action}:`, entry.nextState);
        });
        console.groupEnd();
      }
    };

    const optimizedSet: SetState<T> = (
      partial,
      replace,
      action = 'unknown'
    ) => {
      set(partial, replace);

      // 軽量なログエントリー
      logBuffer.push({
        timestamp: new Date(),
        action,
        prevState: null, // パフォーマンスのため省略
        nextState: get(),
      });

      // バッファサイズチェック
      if (logBuffer.length >= config.batchSize) {
        flushLogs();
      } else {
        // タイマーでの定期フラッシュ
        if (flushTimer) clearTimeout(flushTimer);
        flushTimer = setTimeout(
          flushLogs,
          config.flushInterval
        );
      }
    };

    return stateCreator(optimizedSet, get, api);
  };

// 使用例
const useHighFrequencyStore = create<GameState>()(
  performantLogger<GameState>({
    batchSize: 10,
    flushInterval: 1000, // 1秒
    asyncLogging: true,
  })((set) => ({
    score: 0,
    position: { x: 0, y: 0 },

    updatePosition: (x: number, y: number) => {
      set({ position: { x, y } }, false, 'updatePosition');
    },

    incrementScore: () => {
      set(
        (state) => ({ score: state.score + 1 }),
        false,
        'incrementScore'
      );
    },
  }))
);

Redux DevTools 連携のベストプラクティス

Zustand の devtools ミドルウェアと組み合わせることで、より強力なデバッグ環境を構築できます。

typescript// DevTools 連携を考慮したロガー
const devToolsLogger =
  <T>(stateCreator: StateCreator<T>): StateCreator<T> =>
  (set, get, api) => {
    const enhancedSet: SetState<T> = (
      partial,
      replace,
      action
    ) => {
      const prevState = get();
      set(partial, replace);
      const nextState = get();

      // Redux DevTools にアクション情報を送信
      if (
        typeof window !== 'undefined' &&
        window.__REDUX_DEVTOOLS_EXTENSION__
      ) {
        const devTools =
          window.__REDUX_DEVTOOLS_EXTENSION__.connect();
        devTools.send(action || 'unknown', nextState);
      }

      // 詳細ログも出力
      console.group(`🔧 DevTools: ${action || 'unknown'}`);
      console.log('Action:', action);
      console.log('State:', nextState);
      console.groupEnd();
    };

    return stateCreator(enhancedSet, get, api);
  };

// DevTools とロガーの組み合わせ
const useDebuggableStore = create<AppState>()(
  devtools(
    devToolsLogger((set, get) => ({
      count: 0,
      user: null,

      increment: () =>
        set(
          (state) => ({ ...state, count: state.count + 1 }),
          false,
          'increment'
        ),

      setUser: (user: User) =>
        set(
          (state) => ({ ...state, user }),
          false,
          'setUser'
        ),
    })),
    { name: 'debuggable-store' }
  )
);

永続化戦略の最適解

アプリケーションの状態を永続化することで、ユーザー体験を大幅に向上させることができます。Zustand の persist ミドルウェアを活用した効果的な永続化戦略を詳しく見ていきましょう。

Persist Middleware の詳細実装

Zustand の persist ミドルウェアは、状態の永続化を簡単に実現できる強力な機能です。

基本的な永続化実装

typescriptimport {
  persist,
  createJSONStorage,
} from 'zustand/middleware';

interface UserPreferences {
  theme: 'light' | 'dark';
  language: 'ja' | 'en';
  notifications: boolean;
  setTheme: (theme: 'light' | 'dark') => void;
  setLanguage: (language: 'ja' | 'en') => void;
  toggleNotifications: () => void;
}

const useUserPreferencesStore = create<UserPreferences>()(
  persist(
    (set, get) => ({
      theme: 'light',
      language: 'ja',
      notifications: true,

      setTheme: (theme) => set({ theme }),
      setLanguage: (language) => set({ language }),
      toggleNotifications: () =>
        set((state) => ({
          notifications: !state.notifications,
        })),
    }),
    {
      name: 'user-preferences',
      storage: createJSONStorage(() => localStorage),
    }
  )
);

高度な永続化設定

typescriptinterface AdvancedPersistConfig {
  name: string;
  storage: any;
  partialize?: (state: any) => any;
  onRehydrateStorage?: (state: any) => void;
  version?: number;
  migrate?: (persistedState: any, version: number) => any;
  merge?: (persistedState: any, currentState: any) => any;
}

const createAdvancedPersist = <T>(
  config: AdvancedPersistConfig
) =>
  persist<T>(
    (set, get) =>
      ({
        // ストアの実装
      } as T),
    {
      name: config.name,
      storage: config.storage,

      // 永続化する状態の選択
      partialize: (state) => {
        if (config.partialize) {
          return config.partialize(state);
        }

        // デフォルトでは関数以外を永続化
        const { ...persistedState } = state;
        Object.keys(persistedState).forEach((key) => {
          if (typeof persistedState[key] === 'function') {
            delete persistedState[key];
          }
        });
        return persistedState;
      },

      // 復元時の処理
      onRehydrateStorage: (state) => {
        console.log('状態復元開始:', state);

        return (state, error) => {
          if (error) {
            console.error('状態復元エラー:', error);
          } else {
            console.log('状態復元完了:', state);
            config.onRehydrateStorage?.(state);
          }
        };
      },

      // バージョン管理
      version: config.version || 1,
      migrate: config.migrate,

      // 状態のマージ戦略
      merge: (persistedState, currentState) => {
        if (config.merge) {
          return config.merge(persistedState, currentState);
        }

        // デフォルトのマージ戦略
        return {
          ...currentState,
          ...persistedState,
        };
      },
    }
  );

localStorage vs IndexedDB vs WebSQL 選択指針

永続化ストレージの選択は、アプリケーションの要件に大きく影響します。

ストレージ比較表

#特徴localStorageIndexedDBWebSQLSessionStorage
1容量制限5-10MB無制限*廃止予定5-10MB
2非同期 API
3構造化データ
4トランザクション
5ブラウザサポート廃止
6実装の簡単さ★★★★★★★☆☆☆★★★☆☆★★★★★

localStorage 実装

typescript// localStorage 用のカスタムストレージ
const localStorageAdapter = {
  getItem: (name: string): string | null => {
    try {
      return localStorage.getItem(name);
    } catch (error) {
      console.error('localStorage 読み込みエラー:', error);
      return null;
    }
  },

  setItem: (name: string, value: string): void => {
    try {
      localStorage.setItem(name, value);
    } catch (error) {
      console.error('localStorage 書き込みエラー:', error);

      // 容量不足の場合の対処
      if (error.name === 'QuotaExceededError') {
        // 古いデータを削除
        const keys = Object.keys(localStorage);
        const oldKeys = keys.filter(
          (key) =>
            key.startsWith('zustand-') && key !== name
        );

        oldKeys.forEach((key) =>
          localStorage.removeItem(key)
        );

        // 再試行
        try {
          localStorage.setItem(name, value);
        } catch (retryError) {
          console.error(
            'localStorage 再試行失敗:',
            retryError
          );
        }
      }
    }
  },

  removeItem: (name: string): void => {
    try {
      localStorage.removeItem(name);
    } catch (error) {
      console.error('localStorage 削除エラー:', error);
    }
  },
};

const useLocalStorageStore = create<AppState>()(
  persist(
    (set, get) => ({
      // ストア実装
    }),
    {
      name: 'app-state',
      storage: createJSONStorage(() => localStorageAdapter),
    }
  )
);

IndexedDB 実装

typescript// IndexedDB 用のカスタムストレージ
const createIndexedDBStorage = (
  dbName: string,
  version: number = 1
) => {
  let db: IDBDatabase | null = null;

  const initDB = (): Promise<IDBDatabase> => {
    return new Promise((resolve, reject) => {
      if (db) {
        resolve(db);
        return;
      }

      const request = indexedDB.open(dbName, version);

      request.onerror = () => reject(request.error);
      request.onsuccess = () => {
        db = request.result;
        resolve(db);
      };

      request.onupgradeneeded = (event) => {
        const database = (event.target as IDBOpenDBRequest)
          .result;

        if (
          !database.objectStoreNames.contains(
            'zustand-store'
          )
        ) {
          database.createObjectStore('zustand-store');
        }
      };
    });
  };

  return {
    getItem: async (
      name: string
    ): Promise<string | null> => {
      try {
        const database = await initDB();
        const transaction = database.transaction(
          ['zustand-store'],
          'readonly'
        );
        const store =
          transaction.objectStore('zustand-store');

        return new Promise((resolve, reject) => {
          const request = store.get(name);
          request.onerror = () => reject(request.error);
          request.onsuccess = () =>
            resolve(request.result || null);
        });
      } catch (error) {
        console.error('IndexedDB 読み込みエラー:', error);
        return null;
      }
    },

    setItem: async (
      name: string,
      value: string
    ): Promise<void> => {
      try {
        const database = await initDB();
        const transaction = database.transaction(
          ['zustand-store'],
          'readwrite'
        );
        const store =
          transaction.objectStore('zustand-store');

        return new Promise((resolve, reject) => {
          const request = store.put(value, name);
          request.onerror = () => reject(request.error);
          request.onsuccess = () => resolve();
        });
      } catch (error) {
        console.error('IndexedDB 書き込みエラー:', error);
      }
    },

    removeItem: async (name: string): Promise<void> => {
      try {
        const database = await initDB();
        const transaction = database.transaction(
          ['zustand-store'],
          'readwrite'
        );
        const store =
          transaction.objectStore('zustand-store');

        return new Promise((resolve, reject) => {
          const request = store.delete(name);
          request.onerror = () => reject(request.error);
          request.onsuccess = () => resolve();
        });
      } catch (error) {
        console.error('IndexedDB 削除エラー:', error);
      }
    },
  };
};

const useIndexedDBStore = create<LargeDataState>()(
  persist(
    (set, get) => ({
      // 大量データを扱うストア実装
    }),
    {
      name: 'large-data-state',
      storage: createIndexedDBStorage('my-app-db'),
    }
  )
);

パーシャル永続化とデータマイグレーション

すべての状態を永続化する必要はありません。適切な選択と移行戦略が重要です。

パーシャル永続化の実装

typescriptinterface AppState {
  // 永続化したいデータ
  user: User | null;
  preferences: UserPreferences;

  // 永続化したくないデータ
  loading: boolean;
  error: string | null;
  temporaryData: any;

  // アクション
  setUser: (user: User) => void;
  setLoading: (loading: boolean) => void;
  setError: (error: string | null) => void;
}

const useAppStore = create<AppState>()(
  persist(
    (set, get) => ({
      user: null,
      preferences: {
        theme: 'light',
        language: 'ja',
        notifications: true,
      },
      loading: false,
      error: null,
      temporaryData: null,

      setUser: (user) => set({ user }),
      setLoading: (loading) => set({ loading }),
      setError: (error) => set({ error }),
    }),
    {
      name: 'app-state',

      // 永続化する状態を選択
      partialize: (state) => ({
        user: state.user,
        preferences: state.preferences,
        // loading, error, temporaryData は除外
      }),

      // 復元時のデフォルト値設定
      merge: (persistedState, currentState) => ({
        ...currentState,
        ...persistedState,
        // 永続化されていない状態はデフォルト値を使用
        loading: false,
        error: null,
        temporaryData: null,
      }),
    }
  )
);

データマイグレーション戦略

typescriptinterface MigrationConfig {
  version: number;
  migrations: Record<number, (state: any) => any>;
}

const createMigratableStore = <T>(
  stateCreator: StateCreator<T>,
  migrationConfig: MigrationConfig
) => {
  return persist(stateCreator, {
    name: 'migratable-store',
    version: migrationConfig.version,

    migrate: (persistedState: any, version: number) => {
      let migratedState = persistedState;

      // バージョンが古い場合、順次マイグレーション実行
      for (
        let v = version;
        v < migrationConfig.version;
        v++
      ) {
        const migration = migrationConfig.migrations[v + 1];
        if (migration) {
          console.log(
            `マイグレーション実行: v${v} -> v${v + 1}`
          );
          migratedState = migration(migratedState);
        }
      }

      return migratedState;
    },
  });
};

// マイグレーション設定例
const migrationConfig: MigrationConfig = {
  version: 3,
  migrations: {
    // v1 -> v2: user.name を user.firstName, user.lastName に分割
    2: (state: any) => ({
      ...state,
      user: state.user
        ? {
            ...state.user,
            firstName: state.user.name?.split(' ')[0] || '',
            lastName: state.user.name?.split(' ')[1] || '',
            name: undefined, // 古いフィールドを削除
          }
        : null,
    }),

    // v2 -> v3: preferences に新しいフィールド追加
    3: (state: any) => ({
      ...state,
      preferences: {
        ...state.preferences,
        autoSave: true, // 新しいデフォルト値
        fontSize: 'medium',
      },
    }),
  },
};

const useMigratableStore = create<AppState>()(
  createMigratableStore(
    (set, get) => ({
      // ストア実装
    }),
    migrationConfig
  )
);

セキュリティ考慮事項(暗号化・プライバシー)

機密データを永続化する場合、適切なセキュリティ対策が必要です。

暗号化ストレージの実装

typescript// 暗号化機能付きストレージ
class EncryptedStorage {
  private key: string;

  constructor(key: string) {
    this.key = key;
  }

  private async encrypt(data: string): Promise<string> {
    // Web Crypto API を使用した暗号化
    const encoder = new TextEncoder();
    const keyMaterial = await crypto.subtle.importKey(
      'raw',
      encoder.encode(this.key),
      { name: 'PBKDF2' },
      false,
      ['deriveBits', 'deriveKey']
    );

    const salt = crypto.getRandomValues(new Uint8Array(16));
    const derivedKey = await crypto.subtle.deriveKey(
      {
        name: 'PBKDF2',
        salt,
        iterations: 100000,
        hash: 'SHA-256',
      },
      keyMaterial,
      { name: 'AES-GCM', length: 256 },
      false,
      ['encrypt']
    );

    const iv = crypto.getRandomValues(new Uint8Array(12));
    const encrypted = await crypto.subtle.encrypt(
      { name: 'AES-GCM', iv },
      derivedKey,
      encoder.encode(data)
    );

    // salt + iv + encrypted data を結合
    const result = new Uint8Array(
      salt.length + iv.length + encrypted.byteLength
    );
    result.set(salt, 0);
    result.set(iv, salt.length);
    result.set(
      new Uint8Array(encrypted),
      salt.length + iv.length
    );

    return btoa(String.fromCharCode(...result));
  }

  private async decrypt(
    encryptedData: string
  ): Promise<string> {
    try {
      const data = new Uint8Array(
        atob(encryptedData)
          .split('')
          .map((char) => char.charCodeAt(0))
      );

      const salt = data.slice(0, 16);
      const iv = data.slice(16, 28);
      const encrypted = data.slice(28);

      const encoder = new TextEncoder();
      const keyMaterial = await crypto.subtle.importKey(
        'raw',
        encoder.encode(this.key),
        { name: 'PBKDF2' },
        false,
        ['deriveBits', 'deriveKey']
      );

      const derivedKey = await crypto.subtle.deriveKey(
        {
          name: 'PBKDF2',
          salt,
          iterations: 100000,
          hash: 'SHA-256',
        },
        keyMaterial,
        { name: 'AES-GCM', length: 256 },
        false,
        ['decrypt']
      );

      const decrypted = await crypto.subtle.decrypt(
        { name: 'AES-GCM', iv },
        derivedKey,
        encrypted
      );

      return new TextDecoder().decode(decrypted);
    } catch (error) {
      console.error('復号化エラー:', error);
      throw new Error('データの復号化に失敗しました');
    }
  }

  getItem = async (
    name: string
  ): Promise<string | null> => {
    try {
      const encryptedData = localStorage.getItem(name);
      if (!encryptedData) return null;

      return await this.decrypt(encryptedData);
    } catch (error) {
      console.error(
        '暗号化ストレージ読み込みエラー:',
        error
      );
      return null;
    }
  };

  setItem = async (
    name: string,
    value: string
  ): Promise<void> => {
    try {
      const encryptedData = await this.encrypt(value);
      localStorage.setItem(name, encryptedData);
    } catch (error) {
      console.error(
        '暗号化ストレージ書き込みエラー:',
        error
      );
    }
  };

  removeItem = (name: string): void => {
    localStorage.removeItem(name);
  };
}

// 暗号化ストレージを使用したストア
const useSecureStore = create<SecureState>()(
  persist(
    (set, get) => ({
      sensitiveData: null,
      publicData: null,

      setSensitiveData: (data) =>
        set({ sensitiveData: data }),
      setPublicData: (data) => set({ publicData: data }),
    }),
    {
      name: 'secure-state',
      storage: new EncryptedStorage('your-encryption-key'),

      // 機密データのみ暗号化ストレージに保存
      partialize: (state) => ({
        sensitiveData: state.sensitiveData,
        // publicData は通常のストレージに保存
      }),
    }
  )
);

プライバシー保護の実装

typescript// プライバシー保護機能付きストレージ
const createPrivacyAwareStorage = (options: {
  anonymizeData?: boolean;
  excludeFields?: string[];
  ttl?: number; // Time To Live (ミリ秒)
}) => {
  const {
    anonymizeData = false,
    excludeFields = [],
    ttl,
  } = options;

  const anonymizeValue = (value: any): any => {
    if (typeof value === 'string') {
      // 文字列の場合、一部をマスク
      return value.length > 3
        ? value.substring(0, 2) +
            '*'.repeat(value.length - 2)
        : '***';
    }

    if (typeof value === 'object' && value !== null) {
      const anonymized: any = {};
      Object.keys(value).forEach((key) => {
        if (excludeFields.includes(key)) {
          // 除外フィールドは保存しない
          return;
        }

        if (
          anonymizeData &&
          ['email', 'phone', 'address'].includes(key)
        ) {
          anonymized[key] = anonymizeValue(value[key]);
        } else {
          anonymized[key] = value[key];
        }
      });
      return anonymized;
    }

    return value;
  };

  return {
    getItem: (name: string): string | null => {
      try {
        const item = localStorage.getItem(name);
        if (!item) return null;

        const parsed = JSON.parse(item);

        // TTL チェック
        if (ttl && parsed.timestamp) {
          const now = Date.now();
          if (now - parsed.timestamp > ttl) {
            localStorage.removeItem(name);
            return null;
          }
        }

        return JSON.stringify(parsed.data);
      } catch (error) {
        console.error(
          'プライバシー保護ストレージ読み込みエラー:',
          error
        );
        return null;
      }
    },

    setItem: (name: string, value: string): void => {
      try {
        const data = JSON.parse(value);
        const processedData = anonymizeValue(data);

        const item = {
          data: processedData,
          timestamp: ttl ? Date.now() : undefined,
        };

        localStorage.setItem(name, JSON.stringify(item));
      } catch (error) {
        console.error(
          'プライバシー保護ストレージ書き込みエラー:',
          error
        );
      }
    },

    removeItem: (name: string): void => {
      localStorage.removeItem(name);
    },
  };
};

// プライバシー保護ストレージを使用したストア
const usePrivacyAwareStore = create<UserState>()(
  persist(
    (set, get) => ({
      user: null,
      preferences: {},

      setUser: (user) => set({ user }),
      setPreferences: (preferences) => set({ preferences }),
    }),
    {
      name: 'privacy-aware-state',
      storage: createJSONStorage(() =>
        createPrivacyAwareStorage({
          anonymizeData: true,
          excludeFields: ['password', 'creditCard'],
          ttl: 24 * 60 * 60 * 1000, // 24時間
        })
      ),
    }
  )
);

開発体験を向上させるミドルウェア集

開発効率を最大化するために、Zustand の豊富なミドルウェアエコシステムを活用しましょう。ここでは、開発体験を劇的に向上させる実用的なミドルウェアを紹介します。

Subscriptions Middleware によるリアクティブ設計

subscribeWithSelector ミドルウェアを使用することで、状態の特定部分の変更を監視し、リアクティブな処理を実装できます。

typescriptimport { subscribeWithSelector } from 'zustand/middleware';

interface ReactiveState {
  user: User | null;
  cart: CartItem[];
  notifications: Notification[];

  setUser: (user: User | null) => void;
  addToCart: (item: CartItem) => void;
  addNotification: (notification: Notification) => void;
}

const useReactiveStore = create<ReactiveState>()(
  subscribeWithSelector((set, get) => ({
    user: null,
    cart: [],
    notifications: [],

    setUser: (user) => set({ user }),
    addToCart: (item) =>
      set((state) => ({
        cart: [...state.cart, item],
      })),
    addNotification: (notification) =>
      set((state) => ({
        notifications: [
          ...state.notifications,
          notification,
        ],
      })),
  }))
);

// 特定の状態変更を監視
const useUserSubscription = () => {
  useEffect(() => {
    // ユーザーログイン/ログアウトを監視
    const unsubscribe = useReactiveStore.subscribe(
      (state) => state.user,
      (user, previousUser) => {
        if (user && !previousUser) {
          console.log('ユーザーがログインしました:', user);
          // ログイン時の処理
          analytics.track('user_login', {
            userId: user.id,
          });
        } else if (!user && previousUser) {
          console.log('ユーザーがログアウトしました');
          // ログアウト時の処理
          analytics.track('user_logout', {
            userId: previousUser.id,
          });
        }
      }
    );

    return unsubscribe;
  }, []);
};

// 複数の状態を組み合わせた監視
const useCartSubscription = () => {
  useEffect(() => {
    const unsubscribe = useReactiveStore.subscribe(
      (state) => ({ cart: state.cart, user: state.user }),
      ({ cart, user }, previous) => {
        // カートの変更とユーザー情報を組み合わせた処理
        if (cart.length > previous.cart.length && user) {
          const newItem = cart[cart.length - 1];
          console.log(
            `${user.name}がカートに商品を追加:`,
            newItem
          );

          // 自動保存
          saveCartToServer(user.id, cart);
        }
      },
      {
        equalityFn: (a, b) =>
          a.cart.length === b.cart.length &&
          a.user?.id === b.user?.id,
      }
    );

    return unsubscribe;
  }, []);
};

高度なサブスクリプションパターン

typescript// カスタムサブスクリプションフック
const createSubscriptionHook = <T, U>(
  selector: (state: T) => U,
  callback: (current: U, previous: U) => void,
  options?: {
    fireImmediately?: boolean;
    equalityFn?: (a: U, b: U) => boolean;
  }
) => {
  return (store: any) => {
    useEffect(() => {
      let previousValue = selector(store.getState());

      if (options?.fireImmediately) {
        callback(previousValue, previousValue);
      }

      const unsubscribe = store.subscribe(
        selector,
        (current: U, previous: U) => {
          callback(current, previous);
          previousValue = current;
        },
        {
          equalityFn: options?.equalityFn || Object.is,
        }
      );

      return unsubscribe;
    }, []);
  };
};

// 使用例
const useNotificationSubscription = createSubscriptionHook(
  (state: ReactiveState) => state.notifications,
  (notifications, previousNotifications) => {
    const newNotifications = notifications.slice(
      previousNotifications.length
    );
    newNotifications.forEach((notification) => {
      if (notification.type === 'error') {
        toast.error(notification.message);
      } else if (notification.type === 'success') {
        toast.success(notification.message);
      }
    });
  },
  {
    fireImmediately: false,
    equalityFn: (a, b) => a.length === b.length,
  }
);

Immer Middleware との連携最適化

Immer ミドルウェアと他のミドルウェアを組み合わせることで、より直感的で安全な状態更新を実現できます。

typescriptimport { immer } from 'zustand/middleware/immer';

interface ComplexState {
  users: Record<string, User>;
  posts: Record<string, Post>;
  comments: Record<string, Comment[]>;
  ui: {
    selectedUserId: string | null;
    selectedPostId: string | null;
    filters: {
      category: string;
      dateRange: [Date, Date] | null;
      tags: string[];
    };
  };
}

const useComplexStore = create<ComplexState>()(
  devtools(
    persist(
      subscribeWithSelector(
        immer((set, get) => ({
          users: {},
          posts: {},
          comments: {},
          ui: {
            selectedUserId: null,
            selectedPostId: null,
            filters: {
              category: 'all',
              dateRange: null,
              tags: [],
            },
          },

          // Immer を使った直感的な状態更新
          addUser: (user: User) =>
            set((state) => {
              state.users[user.id] = user;
            }),

          updateUser: (
            userId: string,
            updates: Partial<User>
          ) =>
            set((state) => {
              if (state.users[userId]) {
                Object.assign(state.users[userId], updates);
              }
            }),

          addPost: (post: Post) =>
            set((state) => {
              state.posts[post.id] = post;
              // 投稿者の投稿数を更新
              if (state.users[post.authorId]) {
                state.users[post.authorId].postCount =
                  (state.users[post.authorId].postCount ||
                    0) + 1;
              }
            }),

          addComment: (postId: string, comment: Comment) =>
            set((state) => {
              if (!state.comments[postId]) {
                state.comments[postId] = [];
              }
              state.comments[postId].push(comment);

              // 投稿のコメント数を更新
              if (state.posts[postId]) {
                state.posts[postId].commentCount =
                  state.comments[postId].length;
              }
            }),

          setFilter: (
            filterType: keyof ComplexState['ui']['filters'],
            value: any
          ) =>
            set((state) => {
              state.ui.filters[filterType] = value;
            }),

          selectUser: (userId: string | null) =>
            set((state) => {
              state.ui.selectedUserId = userId;
            }),

          selectPost: (postId: string | null) =>
            set((state) => {
              state.ui.selectedPostId = postId;
            }),
        }))
      ),
      { name: 'complex-state' }
    ),
    { name: 'complex-store' }
  )
);

Immer との型安全性向上

typescript// Immer 用の型安全なヘルパー
type ImmerStateCreator<T> = (
  set: (fn: (draft: T) => void) => void,
  get: () => T
) => T;

const createImmerStore = <T>(
  stateCreator: ImmerStateCreator<T>
) => {
  return create<T>()(
    devtools(
      persist(immer(stateCreator as any), {
        name: 'immer-store',
      }),
      { name: 'immer-devtools' }
    )
  );
};

// 型安全な使用例
interface TypeSafeState {
  nested: {
    deeply: {
      value: number;
      items: string[];
    };
  };
  updateNestedValue: (value: number) => void;
  addItem: (item: string) => void;
}

const useTypeSafeStore = createImmerStore<TypeSafeState>(
  (set, get) => ({
    nested: {
      deeply: {
        value: 0,
        items: [],
      },
    },

    updateNestedValue: (value) =>
      set((draft) => {
        draft.nested.deeply.value = value; // 型安全
      }),

    addItem: (item) =>
      set((draft) => {
        draft.nested.deeply.items.push(item); // 型安全
      }),
  })
);

Time Travel Debugging の実装

開発中の状態変更を記録し、任意の時点に戻ることができる Time Travel Debugging を実装しましょう。

typescriptinterface TimeTravelState<T> {
  history: T[];
  currentIndex: number;
  maxHistorySize: number;
}

const createTimeTravelMiddleware = <T>(maxHistorySize: number = 50) =>
  (stateCreator: StateCreator<T>): StateCreator<T & TimeTravelState<T>> =>
    (set, get, api) => {
      const initialState = stateCreator(
        (partial, replace, action) => {
          const currentState = get();
          const newState = typeof partial === 'function'
            ? (partial as any)(currentState)
            : partial;

          // 履歴に追加
          const history = [...currentState.history];
          const currentIndex = currentState.currentIndex;

          // 現在位置より後の履歴を削除(新しい分岐)
          if (currentIndex < history.length - 1) {
            history.splice(currentIndex + 1);
          }

          // 新しい状態を履歴に追加
          history.push({ ...currentState, ...newState });

          // 最大サイズを超えた場合、古い履歴を削除
          if (history.length > maxHistorySize) {
            history.shift();
          }

          set({
            ...newState,
            history,
            currentIndex: history.length - 1,
          } as any, replace);
        },
        get,
        api
      );

      return {
        ...initialState,
        history: [initialState],
        currentIndex: 0,
        maxHistorySize,

        // Time Travel 機能
        undo: () => {
          const { history, currentIndex } = get();
          if (currentIndex > 0) {
            const previousState = history[currentIndex - 1];
            set({
              ...previousState,
              currentIndex: currentIndex - 1,
            } as any);
          }
        },

        redo: () => {
          const { history, currentIndex } = get();
          if (currentIndex < history.length - 1) {
            const nextState = history[currentIndex + 1];
            set({
              ...nextState,
              currentIndex: currentIndex + 1,
            } as any);
          }
        },

        jumpTo: (index: number) => {
          const { history } = get();
          if (index >= 0 && index < history.length) {
            const targetState = history[index];
            set({
              ...targetState,
              currentIndex: index,
            } as any);
          }
        },

        clearHistory: () => {
          const currentState = get();
          const { history, currentIndex, maxHistorySize, ...stateWithoutTimeTravel } = currentState;
          set({
            ...stateWithoutTimeTravel,
            history: [stateWithoutTimeTravel],
            currentIndex: 0,
          } as any);
        },

        canUndo: () => get().currentIndex > 0,
        canRedo: () => get().currentIndex < get().history.length - 1,
      } as T & TimeTravelState<T>;
    };

// Time Travel 対応ストアの作成
interface AppState {
  count: number;
  text: string;
  increment: () => void;
  decrement: () => void;
  setText: (text: string) => void;
}

const useTimeTravelStore = create<AppState & TimeTravelState<AppState>>()(
  createTimeTravelMiddleware<AppState>()(
    (set) => ({
      count: 0,
      text: '',

      increment: () =>
        set((state) => ({ count: state.count + 1 })),
      decrement: () => set(state => ({ count: state.count - 1 })),
      setText: (text) => set({ text }),
    })
  )
);

// Time Travel UI コンポーネント
const TimeTravelControls: React.FC = () => {
  const { undo, redo, canUndo, canRedo, history, currentIndex, jumpTo } =
    useTimeTravelStore();

  return (
    <div className="time-travel-controls">
      <button onClick={undo} disabled={!canUndo()}>
        ⏪ Undo
      </button>
      <button onClick={redo} disabled={!canRedo()}>
        ⏩ Redo
      </button>

      <div className="history-timeline">
        {history.map((state, index) => (
          <button
            key={index}
            className={index === currentIndex ? 'active' : ''}
            onClick={() => jumpTo(index)}
          >
            {index}
          </button>
        ))}
      </div>
    </div>
  );
};

Hot Reloading サポート

開発中のコード変更時に状態を保持する Hot Reloading サポートを実装します。

typescript// Hot Reloading 対応ミドルウェア
const createHotReloadMiddleware =
  <T>(storeName: string, preserveState: boolean = true) =>
  (stateCreator: StateCreator<T>): StateCreator<T> =>
  (set, get, api) => {
    // 開発環境でのみ有効
    if (process.env.NODE_ENV !== 'development') {
      return stateCreator(set, get, api);
    }

    // Hot Reloading 時の状態保存
    const saveStateForHotReload = () => {
      if (preserveState && typeof window !== 'undefined') {
        const currentState = get();
        (window as any).__ZUSTAND_HOT_RELOAD_STATE__ = {
          ...(window as any).__ZUSTAND_HOT_RELOAD_STATE__,
          [storeName]: currentState,
        };
      }
    };

    // Hot Reloading 時の状態復元
    const restoreStateFromHotReload =
      (): Partial<T> | null => {
        if (
          preserveState &&
          typeof window !== 'undefined'
        ) {
          const hotReloadState = (window as any)
            .__ZUSTAND_HOT_RELOAD_STATE__;
          return hotReloadState?.[storeName] || null;
        }
        return null;
      };

    // 初期状態の作成
    const initialState = stateCreator(set, get, api);
    const restoredState = restoreStateFromHotReload();

    // 状態変更時に自動保存
    const enhancedSet: SetState<T> = (
      partial,
      replace,
      action
    ) => {
      set(partial, replace);
      saveStateForHotReload();
    };

    // Hot Module Replacement の設定
    if (module.hot) {
      module.hot.accept();
      module.hot.dispose(() => {
        saveStateForHotReload();
      });
    }

    // 復元された状態があれば使用、なければ初期状態
    return restoredState
      ? { ...initialState, ...restoredState }
      : initialState;
  };

// Hot Reloading 対応ストア
const useHotReloadStore = create<AppState>()(
  createHotReloadMiddleware<AppState>('app-store')(
    (set, get) => ({
      count: 0,
      user: null,

      increment: () =>
        set((state) => ({ count: state.count + 1 })),
      setUser: (user) => set({ user }),
    })
  )
);

開発ツール統合

typescript// 開発ツール統合ミドルウェア
const createDevToolsIntegration = <T>(options: {
  name: string;
  enableLogging?: boolean;
  enableTimeTravel?: boolean;
  enableHotReload?: boolean;
}) => {
  const middlewares: any[] = [];

  // DevTools
  middlewares.push(
    devtools(undefined, { name: options.name })
  );

  // ログ機能
  if (options.enableLogging) {
    middlewares.push(createEnvironmentAwareLogger<T>());
  }

  // Time Travel
  if (options.enableTimeTravel) {
    middlewares.push(createTimeTravelMiddleware<T>());
  }

  // Hot Reload
  if (options.enableHotReload) {
    middlewares.push(
      createHotReloadMiddleware<T>(options.name)
    );
  }

  // ミドルウェアを順次適用
  return (stateCreator: StateCreator<T>) => {
    return middlewares.reduce(
      (acc, middleware) => middleware(acc),
      stateCreator
    );
  };
};

// 統合開発ストア
const useDevStore = create<AppState>()(
  createDevToolsIntegration<AppState>({
    name: 'dev-store',
    enableLogging: true,
    enableTimeTravel: true,
    enableHotReload: true,
  })((set, get) => ({
    // ストア実装
  }))
);

カスタムミドルウェア開発パターン

Zustand の真の力は、独自のミドルウェアを作成できることにあります。プロジェクト固有の要件に合わせたカスタムミドルウェアの開発方法を詳しく見ていきましょう。

ミドルウェア開発の設計原則

効果的なミドルウェアを開発するための基本原則を理解しましょう。

単一責任の原則

typescript// 悪い例:複数の責任を持つミドルウェア
const badMiddleware =
  <T>(stateCreator: StateCreator<T>) =>
  (set, get, api) => {
    // ログ、永続化、バリデーション、キャッシュなど複数の責任
    return stateCreator(
      (partial, replace, action) => {
        // 複雑で保守困難なコード
        console.log('logging...');
        localStorage.setItem(
          'state',
          JSON.stringify(get())
        );
        validateState(partial);
        updateCache(partial);
        set(partial, replace);
      },
      get,
      api
    );
  };

// 良い例:単一責任のミドルウェア
const createValidationMiddleware =
  <T>(validator: (state: T) => string[]) =>
  (stateCreator: StateCreator<T>): StateCreator<T> =>
  (set, get, api) => {
    const enhancedSet: SetState<T> = (
      partial,
      replace,
      action
    ) => {
      const currentState = get();
      const newState =
        typeof partial === 'function'
          ? (partial as any)(currentState)
          : { ...currentState, ...partial };

      const errors = validator(newState);
      if (errors.length > 0) {
        console.warn('バリデーションエラー:', errors);
        throw new Error(
          `Validation failed: ${errors.join(', ')}`
        );
      }

      set(partial, replace);
    };

    return stateCreator(enhancedSet, get, api);
  };

設定可能性の原則

typescript// 設定可能なミドルウェアの設計
interface MiddlewareConfig {
  enabled?: boolean;
  debug?: boolean;
  [key: string]: any;
}

const createConfigurableMiddleware = <
  T,
  C extends MiddlewareConfig
>(
  defaultConfig: C,
  middlewareFactory: (
    config: C
  ) => (stateCreator: StateCreator<T>) => StateCreator<T>
) => {
  return (userConfig: Partial<C> = {}) => {
    const config = { ...defaultConfig, ...userConfig };

    if (!config.enabled) {
      // 無効化されている場合は何もしない
      return (stateCreator: StateCreator<T>) =>
        stateCreator;
    }

    return middlewareFactory(config);
  };
};

// 使用例
interface CacheConfig extends MiddlewareConfig {
  maxSize: number;
  ttl: number;
  strategy: 'lru' | 'fifo';
}

const createCacheMiddleware = createConfigurableMiddleware<
  any,
  CacheConfig
>(
  {
    enabled: true,
    debug: false,
    maxSize: 100,
    ttl: 5 * 60 * 1000,
    strategy: 'lru',
  },
  (config) => (stateCreator) => (set, get, api) => {
    const cache = new Map();

    if (config.debug) {
      console.log(
        'Cache middleware initialized with config:',
        config
      );
    }

    // キャッシュロジックの実装
    return stateCreator(set, get, api);
  }
);

型安全なミドルウェア作成

TypeScript の型システムを活用して、型安全なミドルウェアを作成しましょう。

高度な型定義

typescript// ミドルウェアの型定義
type MiddlewareImpl<T, Mps = {}, Mcs = {}> = (
  stateCreator: StateCreator<T, [], [], T>,
  ...args: any[]
) => StateCreator<T, [], Mps, T & Mcs>;

// 型安全なミドルウェア作成ヘルパー
const createTypedMiddleware = <
  T,
  MiddlewareProps = {},
  MiddlewareComputedState = {}
>(
  middleware: MiddlewareImpl<
    T,
    MiddlewareProps,
    MiddlewareComputedState
  >
) => middleware;

// 実装例:型安全なメトリクスミドルウェア
interface MetricsState {
  _metrics: {
    actionCount: number;
    lastActionTime: Date | null;
    actionHistory: string[];
  };
}

interface MetricsActions {
  getMetrics: () => MetricsState['_metrics'];
  resetMetrics: () => void;
}

const metricsMiddleware = createTypedMiddleware<
  any,
  {},
  MetricsState & MetricsActions
>((stateCreator) => (set, get, api) => {
  const metrics = {
    actionCount: 0,
    lastActionTime: null as Date | null,
    actionHistory: [] as string[],
  };

  const enhancedSet: SetState<any> = (
    partial,
    replace,
    action
  ) => {
    // メトリクス更新
    metrics.actionCount++;
    metrics.lastActionTime = new Date();
    if (action) {
      metrics.actionHistory.push(action);
      if (metrics.actionHistory.length > 100) {
        metrics.actionHistory.shift();
      }
    }

    set(partial, replace);
  };

  const initialState = stateCreator(enhancedSet, get, api);

  return {
    ...initialState,
    _metrics: metrics,

    getMetrics: () => ({ ...metrics }),
    resetMetrics: () => {
      metrics.actionCount = 0;
      metrics.lastActionTime = null;
      metrics.actionHistory = [];
    },
  };
});

// 型安全な使用
interface AppState {
  count: number;
  increment: () => void;
}

const useMetricsStore = create<
  AppState & MetricsState & MetricsActions
>()(
  metricsMiddleware((set) => ({
    count: 0,
    increment: () =>
      set((state) => ({ count: state.count + 1 })),
  }))
);

条件付き型を使った高度なミドルウェア

typescript// 条件付き型を使ったミドルウェア
type ConditionalMiddleware<
  T,
  Condition extends boolean
> = Condition extends true ? T & { debugInfo: any } : T;

const createConditionalDebugMiddleware =
  <T, Debug extends boolean>(enableDebug: Debug) =>
  (
    stateCreator: StateCreator<T>
  ): StateCreator<ConditionalMiddleware<T, Debug>> =>
  (set, get, api) => {
    const initialState = stateCreator(set, get, api);

    if (enableDebug) {
      return {
        ...initialState,
        debugInfo: {
          createdAt: new Date(),
          version: '1.0.0',
          getStateSnapshot: () => structuredClone(get()),
        },
      } as ConditionalMiddleware<T, Debug>;
    }

    return initialState as ConditionalMiddleware<T, Debug>;
  };

// 使用例
const useConditionalStore = create(
  createConditionalDebugMiddleware(true)(
    // Debug = true
    (set) => ({
      count: 0,
      increment: () =>
        set((state) => ({ count: state.count + 1 })),
    })
  )
);

// TypeScript が debugInfo の存在を認識
const debugInfo = useConditionalStore.getState().debugInfo; // 型安全

パフォーマンス最適化テクニック

ミドルウェアのパフォーマンスを最適化するための実践的なテクニックを紹介します。

遅延初期化パターン

typescript// 遅延初期化を使ったパフォーマンス最適化
const createLazyMiddleware =
  <T>(heavyInitialization: () => any) =>
  (stateCreator: StateCreator<T>): StateCreator<T> =>
  (set, get, api) => {
    let lazyResource: any = null;

    const getLazyResource = () => {
      if (lazyResource === null) {
        console.log('重い初期化処理を実行中...');
        lazyResource = heavyInitialization();
      }
      return lazyResource;
    };

    const enhancedSet: SetState<T> = (
      partial,
      replace,
      action
    ) => {
      // 必要な時だけリソースを初期化
      if (action === 'heavy-operation') {
        const resource = getLazyResource();
        // リソースを使った処理
      }

      set(partial, replace);
    };

    return stateCreator(enhancedSet, get, api);
  };

メモ化とキャッシュ

typescript// メモ化を使ったパフォーマンス最適化
const createMemoizedMiddleware =
  <T>() =>
  (stateCreator: StateCreator<T>): StateCreator<T> =>
  (set, get, api) => {
    const memoCache = new Map<string, any>();

    const memoizedComputation = <R>(
      key: string,
      computation: () => R,
      dependencies: any[]
    ): R => {
      const depKey = `${key}:${JSON.stringify(
        dependencies
      )}`;

      if (memoCache.has(depKey)) {
        return memoCache.get(depKey);
      }

      const result = computation();
      memoCache.set(depKey, result);

      // キャッシュサイズ制限
      if (memoCache.size > 100) {
        const firstKey = memoCache.keys().next().value;
        memoCache.delete(firstKey);
      }

      return result;
    };

    const initialState = stateCreator(set, get, api);

    return {
      ...initialState,
      memoized: memoizedComputation,
    };
  };

非同期処理の最適化

typescript// 非同期処理を最適化するミドルウェア
const createAsyncMiddleware =
  <T>() =>
  (stateCreator: StateCreator<T>): StateCreator<T> =>
  (set, get, api) => {
    const pendingPromises = new Map<string, Promise<any>>();

    const optimizedAsyncAction = async <R>(
      key: string,
      asyncFn: () => Promise<R>,
      options: {
        deduplicate?: boolean;
        timeout?: number;
        retry?: number;
      } = {}
    ): Promise<R> => {
      const {
        deduplicate = true,
        timeout = 10000,
        retry = 0,
      } = options;

      // 重複リクエストの排除
      if (deduplicate && pendingPromises.has(key)) {
        return pendingPromises.get(key);
      }

      const executeWithRetry = async (
        attemptsLeft: number
      ): Promise<R> => {
        try {
          const timeoutPromise = new Promise<never>(
            (_, reject) =>
              setTimeout(
                () => reject(new Error('Timeout')),
                timeout
              )
          );

          const result = await Promise.race([
            asyncFn(),
            timeoutPromise,
          ]);
          return result;
        } catch (error) {
          if (attemptsLeft > 0) {
            console.log(
              `リトライ中... 残り${attemptsLeft}回`
            );
            await new Promise((resolve) =>
              setTimeout(resolve, 1000)
            );
            return executeWithRetry(attemptsLeft - 1);
          }
          throw error;
        }
      };

      const promise = executeWithRetry(retry);

      if (deduplicate) {
        pendingPromises.set(key, promise);
        promise.finally(() => pendingPromises.delete(key));
      }

      return promise;
    };

    const initialState = stateCreator(set, get, api);

    return {
      ...initialState,
      asyncAction: optimizedAsyncAction,
    };
  };

テスト戦略とモッキング

ミドルウェアの品質を保証するためのテスト戦略を紹介します。

ミドルウェアのユニットテスト

typescript// テスト用のヘルパー関数
const createTestStore = <T>(
  stateCreator: StateCreator<T>,
  middlewares: any[] = []
) => {
  const composedMiddleware = middlewares.reduce(
    (acc, middleware) => middleware(acc),
    stateCreator
  );

  return create<T>()(composedMiddleware);
};

// ミドルウェアのテスト例
describe('LoggingMiddleware', () => {
  let consoleSpy: jest.SpyInstance;

  beforeEach(() => {
    consoleSpy = jest
      .spyOn(console, 'log')
      .mockImplementation();
  });

  afterEach(() => {
    consoleSpy.mockRestore();
  });

  it('should log state changes', () => {
    const useStore = createTestStore(
      (set) => ({
        count: 0,
        increment: () =>
          set((state) => ({ count: state.count + 1 })),
      }),
      [advancedLogger({ enabled: true, level: 'info' })]
    );

    const { increment } = useStore.getState();
    increment();

    expect(consoleSpy).toHaveBeenCalled();
    expect(useStore.getState().count).toBe(1);
  });

  it('should not log when disabled', () => {
    const useStore = createTestStore(
      (set) => ({
        count: 0,
        increment: () =>
          set((state) => ({ count: state.count + 1 })),
      }),
      [advancedLogger({ enabled: false })]
    );

    const { increment } = useStore.getState();
    increment();

    expect(consoleSpy).not.toHaveBeenCalled();
  });
});

モッキング戦略

typescript// ストレージのモック
const createMockStorage = () => {
  const storage = new Map<string, string>();

  return {
    getItem: jest.fn(
      (key: string) => storage.get(key) || null
    ),
    setItem: jest.fn((key: string, value: string) => {
      storage.set(key, value);
    }),
    removeItem: jest.fn((key: string) => {
      storage.delete(key);
    }),
    clear: jest.fn(() => {
      storage.clear();
    }),
  };
};

// 永続化ミドルウェアのテスト
describe('PersistMiddleware', () => {
  let mockStorage: ReturnType<typeof createMockStorage>;

  beforeEach(() => {
    mockStorage = createMockStorage();
    (global as any).localStorage = mockStorage;
  });

  it('should persist state to storage', () => {
    const useStore = createTestStore(
      (set) => ({
        count: 0,
        increment: () =>
          set((state) => ({ count: state.count + 1 })),
      }),
      [persist(undefined, { name: 'test-store' })]
    );

    const { increment } = useStore.getState();
    increment();

    expect(mockStorage.setItem).toHaveBeenCalledWith(
      'test-store',
      expect.stringContaining('"count":1')
    );
  });

  it('should restore state from storage', () => {
    // 事前にストレージにデータを設定
    mockStorage.setItem(
      'test-store',
      JSON.stringify({ count: 5 })
    );

    const useStore = createTestStore(
      (set) => ({
        count: 0,
        increment: () =>
          set((state) => ({ count: state.count + 1 })),
      }),
      [persist(undefined, { name: 'test-store' })]
    );

    // 復元された状態を確認
    expect(useStore.getState().count).toBe(5);
  });
});

まとめ

Zustand のミドルウェアシステムは、React アプリケーションの状態管理を次のレベルに押し上げる強力な機能です。本記事で紹介した内容をまとめると:

ログ機能の活用

  • 環境に応じた適切なログ設定
  • パフォーマンスを考慮したバッチ処理
  • Redux DevTools との連携

永続化戦略

  • ストレージの特性に応じた選択
  • パーシャル永続化による最適化
  • セキュリティとプライバシーの考慮

開発体験の向上

  • リアクティブな状態監視
  • Time Travel Debugging
  • Hot Reloading サポート

カスタムミドルウェア開発

  • 設計原則に基づいた実装
  • 型安全性の確保
  • パフォーマンス最適化
  • 包括的なテスト戦略

これらのテクニックを活用することで、保守性が高く、開発効率の良い状態管理システムを構築できます。Zustand のシンプルさを保ちながら、エンタープライズレベルの要求にも対応できる柔軟性を手に入れることができるでしょう。

ミドルウェアの真の価値は、プロジェクトの成長と共に発揮されます。小さく始めて、必要に応じて機能を追加していく段階的なアプローチを心がけ、チーム全体の開発体験向上を目指していきましょう。

関連リンク