T-CREATOR

Zustandの分割ストア設計:スケーラブルな状態管理の秘訣

Zustandの分割ストア設計:スケーラブルな状態管理の秘訣

大規模な Web アプリケーション開発において、状態管理の設計は成功の鍵を握る重要な要素です。特に複数のチームが協力して開発を進める場合、適切に分割されたストア設計がなければ、コードの保守性が低下し、開発効率が大幅に悪化してしまいます。Zustand はその柔軟性とシンプルさにより、様々な分割戦略を採用できる優れた状態管理ライブラリです。この記事では、スケーラブルなアプリケーションを構築するための Zustand 分割ストア設計について、アーキテクチャパターンを交えながら詳しく解説します。

背景

モノリシックストアの限界

従来の状態管理では、アプリケーション全体の状態を単一の大きなストアで管理するモノリシックなアプローチが一般的でした。しかし、アプリケーションが成長するにつれて、この設計にはいくつかの深刻な問題が浮上してきます。

最も顕著な問題は、すべての状態が一箇所に集約されることで生じる責任の集中です。ユーザー情報、商品データ、UI 状態、設定情報など、本来異なる責任を持つデータが同じストア内に混在することで、コードの理解が困難になります。また、一つの機能を修正する際に、意図せず他の機能に影響を与えてしまうリスクも高まります。

パフォーマンスの観点でも、モノリシックストアは問題を抱えています。状態の一部が更新された際に、その変更に関係のないコンポーネントまで再レンダリングが発生してしまうことがあります。これは特に大規模なアプリケーションでは深刻なパフォーマンス劣化を引き起こします。

チーム開発の観点では、複数の開発者が同じストアを同時に編集することで競合が発生しやすくなります。機能開発が進むにつれて、マージ時のコンフリクトが頻繁に起こり、開発効率が著しく低下する傾向にあります。

マイクロストア思想の台頭

これらの課題を解決するアプローチとして、マイクロストア思想が注目されています。この考え方では、アプリケーションの状態を機能や責任ごとに分割し、小さな独立したストアとして管理します。

マイクロストアアプローチの最大の利点は、関心の分離が徹底されることです。各ストアは特定の責任範囲のみを担当するため、コードの理解が容易になり、保守性が大幅に向上します。また、機能ごとに独立してテストを行えるため、品質向上にも寄与します。

Zustand は、この思想を実現するのに理想的なライブラリです。軽量でありながら強力な機能を提供し、複数のストアを効率的に管理できる仕組みを備えています。

課題

大規模アプリケーションでの状態管理の複雑化

大規模アプリケーションでは、状態の種類と量が急激に増加します。ユーザー認証状態、権限情報、ビジネスデータ、UI 状態、キャッシュデータなど、多岐にわたる状態を一元管理することは現実的ではありません。

特に複雑なのは、状態間の依存関係です。例えば、ユーザーの権限によって表示できるデータが変わる場合、権限ストアとデータストアの間に依存関係が生まれます。この依存関係を適切に管理しないと、データの整合性が保てなくなったり、予期しない副作用が発生したりします。

また、状態のライフサイクル管理も重要な課題です。一部の状態はアプリケーション全体で永続的に保持すべきですが、他の状態はページ遷移時にクリアすべきです。このライフサイクルの違いを考慮した設計が必要になります。

パフォーマンス問題と再レンダリングの最適化

不適切な状態管理設計は、深刻なパフォーマンス問題を引き起こします。最も一般的な問題は、過度な再レンダリングです。

例えば、リアルタイムで更新される通知数とメインコンテンツの状態が同じストアに存在する場合、通知数の更新のたびにメインコンテンツも再レンダリングされてしまいます。これは明らかに非効率です。

また、選択的サブスクリプションの実装も課題となります。コンポーネントが必要とする状態のみを監視し、関係のない状態変更には反応しないような仕組みが必要です。

チーム開発での保守性の確保

複数のチームが同じアプリケーションを開発する場合、状態管理の設計は開発効率に直結します。責任範囲の明確化ができていないと、チーム間での調整コストが増大し、開発速度が低下します。

さらに、コードレビューの効率性も重要です。変更の影響範囲が明確でないと、レビュアーはより慎重にコードを検証する必要があり、開発サイクルが遅くなってしまいます。

テスト容易性の向上

大規模アプリケーションでは、テストの自動化が品質維持の要となります。しかし、状態が複雑に絡み合っていると、単体テストや統合テストの実装が困難になります。

特に困難なのは、状態の初期化とクリーンアップです。テスト間での状態の独立性を保ちつつ、現実的なテストシナリオを実装するには、適切に分割された状態管理が不可欠です。

解決策

ドメイン駆動設計によるストア分割

ビジネスドメインごとの境界設定

ドメイン駆動設計(DDD)の考え方を Zustand のストア設計に適用することで、自然で保守しやすい分割を実現できます。まず、ビジネスドメインごとに明確な境界を設定します。

typescript// ユーザードメイン
export const useUserDomainStore = create((set, get) => ({
  // ユーザー認証
  currentUser: null,
  isAuthenticated: false,

  // ユーザープロフィール
  profile: null,
  preferences: {},

  // 認証関連アクション
  login: async (credentials) => {
    // 認証ロジック
    const user = await authService.login(credentials);
    set({ currentUser: user, isAuthenticated: true });
  },

  updateProfile: async (profileData) => {
    // プロフィール更新ロジック
    const updatedProfile = await userService.updateProfile(
      profileData
    );
    set({ profile: updatedProfile });
  },
}));

// 商品ドメイン
export const useProductDomainStore = create((set, get) => ({
  // 商品一覧
  products: [],
  categories: [],

  // 検索・フィルタ状態
  searchQuery: '',
  selectedCategory: null,
  priceRange: { min: 0, max: 1000 },

  // 商品関連アクション
  fetchProducts: async (filters) => {
    const products = await productService.getProducts(
      filters
    );
    set({ products });
  },

  searchProducts: async (query) => {
    set({ searchQuery: query });
    const searchResults = await productService.search(
      query
    );
    set({ products: searchResults });
  },
}));

// 注文ドメイン
export const useOrderDomainStore = create((set, get) => ({
  // カート状態
  cartItems: [],
  totalAmount: 0,

  // 注文履歴
  orders: [],
  currentOrder: null,

  // 注文関連アクション
  addToCart: (product, quantity) => {
    set((state) => {
      const existingItem = state.cartItems.find(
        (item) => item.id === product.id
      );
      const updatedItems = existingItem
        ? state.cartItems.map((item) =>
            item.id === product.id
              ? {
                  ...item,
                  quantity: item.quantity + quantity,
                }
              : item
          )
        : [...state.cartItems, { ...product, quantity }];

      const total = updatedItems.reduce(
        (sum, item) => sum + item.price * item.quantity,
        0
      );

      return {
        cartItems: updatedItems,
        totalAmount: total,
      };
    });
  },

  createOrder: async () => {
    const { cartItems } = get();
    const order = await orderService.createOrder(cartItems);
    set({
      currentOrder: order,
      cartItems: [],
      totalAmount: 0,
    });
    return order;
  },
}));

集約単位でのストア設計

DDD の集約(Aggregate)概念を活用して、関連するエンティティをまとめたストアを設計します:

typescript// ユーザー集約ストア
export const useUserAggregateStore = create((set, get) => ({
  // 集約ルート:User
  user: {
    id: null,
    email: '',
    profile: {
      name: '',
      avatar: null,
      preferences: {
        theme: 'light',
        language: 'ja',
        notifications: {
          email: true,
          push: false,
        },
      },
    },
    subscription: {
      plan: 'free',
      expiresAt: null,
      features: [],
    },
  },

  // 整合性を保つためのビジネスルール
  updateUserProfile: (profileData) => {
    set((state) => ({
      user: {
        ...state.user,
        profile: {
          ...state.user.profile,
          ...profileData,
          // ビジネスルール:名前は必須
          name: profileData.name || state.user.profile.name,
        },
      },
    }));
  },

  upgradeSubscription: (plan) => {
    set((state) => {
      // ビジネスルール:プラン変更時の特別処理
      const features = getFeaturesByPlan(plan);
      const expiresAt = new Date();
      expiresAt.setMonth(expiresAt.getMonth() + 1);

      return {
        user: {
          ...state.user,
          subscription: {
            plan,
            expiresAt: expiresAt.toISOString(),
            features,
          },
        },
      };
    });
  },
}));

ドメインサービスとの連携

ドメインサービスとストアを連携させることで、ビジネスロジックを適切に分離できます:

typescript// ドメインサービス
class OrderDomainService {
  static calculateShipping(items, address) {
    // 配送料計算ロジック
    const weight = items.reduce(
      (sum, item) => sum + item.weight * item.quantity,
      0
    );
    const distance = this.calculateDistance(address);
    return Math.max(500, weight * 10 + distance * 5);
  }

  static validateOrder(items, paymentMethod) {
    // 注文検証ロジック
    if (items.length === 0)
      throw new Error('カートが空です');
    if (!paymentMethod)
      throw new Error('支払い方法を選択してください');
    return true;
  }
}

// ドメインサービスを活用するストア
export const useOrderDomainStore = create((set, get) => ({
  items: [],
  shippingAddress: null,
  paymentMethod: null,
  shippingCost: 0,

  setShippingAddress: (address) => {
    set({ shippingAddress: address });
    // ドメインサービスを使用して配送料を計算
    const { items } = get();
    const shippingCost =
      OrderDomainService.calculateShipping(items, address);
    set({ shippingCost });
  },

  processOrder: async () => {
    const { items, paymentMethod } = get();

    // ドメインサービスで検証
    OrderDomainService.validateOrder(items, paymentMethod);

    // 注文処理
    const order = await orderService.createOrder({
      items,
      shippingAddress: get().shippingAddress,
      paymentMethod,
      shippingCost: get().shippingCost,
    });

    // 成功時の状態更新
    set({
      items: [],
      shippingCost: 0,
      currentOrder: order,
    });

    return order;
  },
}));

レイヤーアーキテクチャの適用

プレゼンテーション層ストア

UI 状態とユーザーインタラクションを管理するストアです:

typescript// UI状態管理ストア
export const useUIStore = create((set, get) => ({
  // グローバルUI状態
  isLoading: false,
  notifications: [],
  modals: {
    confirmDialog: { isOpen: false, content: null },
    userProfile: { isOpen: false },
    productDetail: { isOpen: false, productId: null },
  },

  // ナビゲーション状態
  currentRoute: '/',
  breadcrumbs: [],

  // UI操作アクション
  showNotification: (message, type = 'info') => {
    const id = Date.now().toString();
    set((state) => ({
      notifications: [
        ...state.notifications,
        { id, message, type, timestamp: Date.now() },
      ],
    }));

    // 自動削除
    setTimeout(() => {
      set((state) => ({
        notifications: state.notifications.filter(
          (n) => n.id !== id
        ),
      }));
    }, 5000);
  },

  openModal: (modalName, data = null) => {
    set((state) => ({
      modals: {
        ...state.modals,
        [modalName]: { isOpen: true, ...data },
      },
    }));
  },

  closeModal: (modalName) => {
    set((state) => ({
      modals: {
        ...state.modals,
        [modalName]: { isOpen: false },
      },
    }));
  },
}));

// ページ固有のUI状態ストア
export const useProductListUIStore = create((set, get) => ({
  // 表示設定
  viewMode: 'grid', // 'grid' | 'list'
  sortBy: 'name',
  sortOrder: 'asc',
  itemsPerPage: 20,
  currentPage: 1,

  // フィルタUI状態
  filtersVisible: false,
  selectedFilters: {
    categories: [],
    priceRange: [0, 1000],
    rating: 0,
  },

  // UI操作アクション
  toggleFilters: () => {
    set((state) => ({
      filtersVisible: !state.filtersVisible,
    }));
  },

  updateSort: (sortBy, sortOrder) => {
    set({ sortBy, sortOrder, currentPage: 1 });
  },

  updateFilters: (filters) => {
    set((state) => ({
      selectedFilters: {
        ...state.selectedFilters,
        ...filters,
      },
      currentPage: 1,
    }));
  },
}));

アプリケーション層ストア

ユースケースとアプリケーションフローを管理するストアです:

typescript// ユーザー登録フローストア
export const useUserRegistrationFlowStore = create(
  (set, get) => ({
    // フロー状態
    currentStep: 'basic-info',
    steps: [
      'basic-info',
      'verification',
      'profile-setup',
      'complete',
    ],
    completedSteps: [],

    // フォームデータ
    formData: {
      basicInfo: { email: '', password: '' },
      verification: { code: '' },
      profile: { name: '', avatar: null },
    },

    // バリデーション状態
    validationErrors: {},
    isSubmitting: false,

    // フロー操作
    nextStep: async () => {
      const { currentStep, steps, formData } = get();
      const currentIndex = steps.indexOf(currentStep);

      // 現在のステップのバリデーション
      const isValid = await validateStep(
        currentStep,
        formData[currentStep]
      );
      if (!isValid) return false;

      // 次のステップへ進む
      if (currentIndex < steps.length - 1) {
        const nextStep = steps[currentIndex + 1];
        set((state) => ({
          currentStep: nextStep,
          completedSteps: [
            ...state.completedSteps,
            currentStep,
          ],
        }));
      }

      return true;
    },

    updateFormData: (step, data) => {
      set((state) => ({
        formData: {
          ...state.formData,
          [step]: { ...state.formData[step], ...data },
        },
      }));
    },

    submitRegistration: async () => {
      set({ isSubmitting: true });

      try {
        const { formData } = get();
        await userService.register({
          ...formData.basicInfo,
          ...formData.profile,
          verificationCode: formData.verification.code,
        });

        set({ currentStep: 'complete' });
        return true;
      } catch (error) {
        set({
          validationErrors: { general: error.message },
          isSubmitting: false,
        });
        return false;
      }
    },
  })
);

// ショッピングカートフローストア
export const useShoppingCartFlowStore = create(
  (set, get) => ({
    // フロー状態
    currentStep: 'cart',
    steps: ['cart', 'shipping', 'payment', 'confirmation'],

    // 各ステップのデータ
    cartSummary: null,
    shippingInfo: null,
    paymentInfo: null,

    // フロー操作
    proceedToShipping: async () => {
      const cartItems =
        useOrderDomainStore.getState().cartItems;
      if (cartItems.length === 0) {
        throw new Error('カートが空です');
      }

      const summary = calculateCartSummary(cartItems);
      set({
        cartSummary: summary,
        currentStep: 'shipping',
      });
    },

    proceedToPayment: (shippingData) => {
      set({
        shippingInfo: shippingData,
        currentStep: 'payment',
      });
    },

    completeOrder: async (paymentData) => {
      set({ paymentInfo: paymentData });

      const orderData = {
        items: get().cartSummary.items,
        shipping: get().shippingInfo,
        payment: paymentData,
      };

      const order = await orderService.createOrder(
        orderData
      );
      set({ currentStep: 'confirmation' });

      return order;
    },
  })
);

ドメイン層ストア

前述のドメイン駆動設計セクションで説明したストアがここに該当します。

インフラストラクチャ層ストア

外部サービスとの連携やキャッシュを管理するストアです:

typescript// APIキャッシュストア
export const useAPICache = create((set, get) => ({
  cache: new Map(),
  cacheConfig: {
    defaultTTL: 5 * 60 * 1000, // 5分
    maxSize: 100,
  },

  get: (key) => {
    const { cache } = get();
    const item = cache.get(key);

    if (!item) return null;

    // TTL チェック
    if (Date.now() > item.expiresAt) {
      cache.delete(key);
      return null;
    }

    return item.data;
  },

  set: (key, data, ttl = null) => {
    const { cache, cacheConfig } = get();
    const expiresAt =
      Date.now() + (ttl || cacheConfig.defaultTTL);

    // サイズ制限チェック
    if (cache.size >= cacheConfig.maxSize) {
      // 最も古いアイテムを削除
      const oldestKey = cache.keys().next().value;
      cache.delete(oldestKey);
    }

    cache.set(key, { data, expiresAt });
    set({ cache: new Map(cache) });
  },

  clear: () => {
    set({ cache: new Map() });
  },
}));

// 外部サービス連携ストア
export const useExternalServicesStore = create(
  (set, get) => ({
    // 接続状態
    connections: {
      analytics: false,
      messaging: false,
      payment: false,
    },

    // サービス固有データ
    analyticsData: null,
    messagingStatus: null,
    paymentMethods: [],

    // 接続管理
    connectService: async (serviceName) => {
      try {
        await externalServices[serviceName].connect();
        set((state) => ({
          connections: {
            ...state.connections,
            [serviceName]: true,
          },
        }));
      } catch (error) {
        console.error(
          `Failed to connect to ${serviceName}:`,
          error
        );
      }
    },

    disconnectService: (serviceName) => {
      externalServices[serviceName].disconnect();
      set((state) => ({
        connections: {
          ...state.connections,
          [serviceName]: false,
        },
      }));
    },

    // データ同期
    syncAnalytics: async () => {
      if (!get().connections.analytics) return;

      const data = await analyticsService.fetchData();
      set({ analyticsData: data });
    },
  })
);

ストア間通信パターン

イベント駆動アーキテクチャ

ストア間の疎結合を実現するイベント駆動パターンです:

typescript// イベントバス
class EventBus {
  private listeners = new Map<string, Function[]>();

  subscribe(event: string, callback: Function) {
    if (!this.listeners.has(event)) {
      this.listeners.set(event, []);
    }
    this.listeners.get(event)!.push(callback);

    // アンサブスクライブ関数を返す
    return () => {
      const callbacks = this.listeners.get(event);
      if (callbacks) {
        const index = callbacks.indexOf(callback);
        if (index > -1) callbacks.splice(index, 1);
      }
    };
  }

  emit(event: string, data?: any) {
    const callbacks = this.listeners.get(event);
    if (callbacks) {
      callbacks.forEach((callback) => callback(data));
    }
  }
}

export const eventBus = new EventBus();

// ユーザーストア(イベント発行側)
export const useUserStore = create((set, get) => ({
  user: null,

  login: async (credentials) => {
    const user = await authService.login(credentials);
    set({ user });

    // ログインイベントを発行
    eventBus.emit('user:logged-in', { user });
  },

  logout: () => {
    const user = get().user;
    set({ user: null });

    // ログアウトイベントを発行
    eventBus.emit('user:logged-out', { user });
  },
}));

// 設定ストア(イベント受信側)
export const useSettingsStore = create((set, get) => {
  // イベントリスナーを設定
  eventBus.subscribe('user:logged-in', ({ user }) => {
    // ユーザー固有の設定を読み込む
    settingsService
      .loadUserSettings(user.id)
      .then((settings) => {
        set({ userSettings: settings });
      });
  });

  eventBus.subscribe('user:logged-out', () => {
    // ユーザー設定をクリア
    set({ userSettings: null });
  });

  return {
    userSettings: null,
    globalSettings: {},

    updateSettings: (newSettings) => {
      set((state) => ({
        userSettings: {
          ...state.userSettings,
          ...newSettings,
        },
      }));

      // 設定更新イベントを発行
      eventBus.emit('settings:updated', newSettings);
    },
  };
});

発行/購読パターン

より高度な発行/購読メカニズムです:

typescript// 購読管理ストア
export const useSubscriptionStore = create((set, get) => ({
  subscriptions: new Map(),

  subscribe: (pattern, callback) => {
    const id = Math.random().toString(36);
    const { subscriptions } = get();

    subscriptions.set(id, { pattern, callback });
    set({ subscriptions: new Map(subscriptions) });

    return () => {
      const { subscriptions } = get();
      subscriptions.delete(id);
      set({ subscriptions: new Map(subscriptions) });
    };
  },

  publish: (event, data) => {
    const { subscriptions } = get();

    subscriptions.forEach(({ pattern, callback }) => {
      if (event.match(pattern)) {
        callback(data, event);
      }
    });
  },
}));

// 使用例
const useProductStore = create((set, get) => {
  // 商品関連イベントを購読
  useSubscriptionStore
    .getState()
    .subscribe(
      /product:(created|updated|deleted)/,
      (data, event) => {
        if (event === 'product:created') {
          set((state) => ({
            products: [...state.products, data.product],
          }));
        } else if (event === 'product:updated') {
          set((state) => ({
            products: state.products.map((p) =>
              p.id === data.product.id ? data.product : p
            ),
          }));
        } else if (event === 'product:deleted') {
          set((state) => ({
            products: state.products.filter(
              (p) => p.id !== data.productId
            ),
          }));
        }
      }
    );

  return {
    products: [],

    createProduct: async (productData) => {
      const product = await productService.create(
        productData
      );

      // イベントを発行
      useSubscriptionStore
        .getState()
        .publish('product:created', { product });
    },
  };
});

メディエーターパターン

複雑なストア間通信を調整するメディエーターです:

typescript// アプリケーションメディエーター
class ApplicationMediator {
  private stores = new Map();

  registerStore(name: string, store: any) {
    this.stores.set(name, store);
  }

  // 複雑なフローを調整
  async handleUserLogin(credentials: any) {
    try {
      // 1. ユーザー認証
      const userStore = this.stores.get('user');
      const user = await userStore
        .getState()
        .login(credentials);

      // 2. ユーザー設定を読み込み
      const settingsStore = this.stores.get('settings');
      await settingsStore
        .getState()
        .loadUserSettings(user.id);

      // 3. パーソナライゼーションデータを読み込み
      const personalizationStore = this.stores.get(
        'personalization'
      );
      await personalizationStore
        .getState()
        .loadPersonalizationData(user.id);

      // 4. 通知設定を初期化
      const notificationStore =
        this.stores.get('notification');
      await notificationStore
        .getState()
        .initializeNotifications(user.id);

      // 5. アナリティクスに送信
      const analyticsStore = this.stores.get('analytics');
      analyticsStore
        .getState()
        .trackEvent('user_login', { userId: user.id });

      return { success: true, user };
    } catch (error) {
      // エラー時は全ストアの状態をクリア
      this.clearAllStores();
      throw error;
    }
  }

  private clearAllStores() {
    this.stores.forEach((store) => {
      if (store.getState().clear) {
        store.getState().clear();
      }
    });
  }
}

export const appMediator = new ApplicationMediator();

// ストアの登録
appMediator.registerStore('user', useUserStore);
appMediator.registerStore('settings', useSettingsStore);
appMediator.registerStore(
  'personalization',
  usePersonalizationStore
);
appMediator.registerStore(
  'notification',
  useNotificationStore
);
appMediator.registerStore('analytics', useAnalyticsStore);

依存性注入

ストア間の依存関係を明示的に管理するパターンです:

typescript// 依存性注入コンテナ
class DIContainer {
  private dependencies = new Map();

  register<T>(key: string, factory: () => T) {
    this.dependencies.set(key, factory);
  }

  get<T>(key: string): T {
    const factory = this.dependencies.get(key);
    if (!factory) {
      throw new Error(`Dependency ${key} not found`);
    }
    return factory();
  }
}

export const container = new DIContainer();

// サービスを登録
container.register('userService', () => new UserService());
container.register(
  'productService',
  () => new ProductService()
);
container.register(
  'orderService',
  () => new OrderService()
);

// 依存性注入を活用するストア作成関数
const createStoreWithDI = <T>(
  storeFactory: (services: any) => T
) => {
  return create<T>(() => {
    const services = {
      userService: container.get('userService'),
      productService: container.get('productService'),
      orderService: container.get('orderService'),
    };

    return storeFactory(services);
  });
};

// 使用例
export const useUserStore = createStoreWithDI(
  (services) => ({
    user: null,

    login: async (credentials) => {
      const user = await services.userService.login(
        credentials
      );
      return { user };
    },

    updateProfile: async (profileData) => {
      const updatedUser =
        await services.userService.updateProfile(
          profileData
        );
      return { user: updatedUser };
    },
  })
);

具体例

E コマースアプリケーションでの実装例

実際の E コマースアプリケーションでの分割ストア設計を見てみましょう:

typescript// 商品カタログストア
export const useProductCatalogStore = create(
  (set, get) => ({
    categories: [],
    products: [],
    featuredProducts: [],
    searchResults: [],

    fetchCategories: async () => {
      const categories =
        await productService.getCategories();
      set({ categories });
    },

    fetchProductsByCategory: async (categoryId) => {
      const products =
        await productService.getProductsByCategory(
          categoryId
        );
      set({ products });
    },

    searchProducts: async (query, filters) => {
      const results = await productService.search(
        query,
        filters
      );
      set({ searchResults: results });
    },
  })
);

// ショッピングカートストア
export const useShoppingCartStore = create((set, get) => ({
  items: [],
  totalQuantity: 0,
  totalAmount: 0,
  appliedCoupons: [],

  addItem: (product, quantity = 1) => {
    set((state) => {
      const existingItemIndex = state.items.findIndex(
        (item) => item.product.id === product.id
      );

      let newItems;
      if (existingItemIndex >= 0) {
        newItems = state.items.map((item, index) =>
          index === existingItemIndex
            ? {
                ...item,
                quantity: item.quantity + quantity,
              }
            : item
        );
      } else {
        newItems = [...state.items, { product, quantity }];
      }

      const totalQuantity = newItems.reduce(
        (sum, item) => sum + item.quantity,
        0
      );
      const totalAmount = newItems.reduce(
        (sum, item) =>
          sum + item.product.price * item.quantity,
        0
      );

      return {
        items: newItems,
        totalQuantity,
        totalAmount,
      };
    });

    // アナリティクスイベント発行
    eventBus.emit('cart:item-added', { product, quantity });
  },

  applyCoupon: async (couponCode) => {
    const coupon = await couponService.validateCoupon(
      couponCode
    );
    if (coupon.isValid) {
      set((state) => ({
        appliedCoupons: [...state.appliedCoupons, coupon],
      }));

      // 金額を再計算
      get().recalculateTotal();
    }
  },

  recalculateTotal: () => {
    const { items, appliedCoupons } = get();
    let total = items.reduce(
      (sum, item) =>
        sum + item.product.price * item.quantity,
      0
    );

    // クーポン割引を適用
    appliedCoupons.forEach((coupon) => {
      total =
        coupon.type === 'percentage'
          ? total * (1 - coupon.value / 100)
          : total - coupon.value;
    });

    set({ totalAmount: Math.max(0, total) });
  },
}));

// 注文管理ストア
export const useOrderManagementStore = create(
  (set, get) => ({
    orders: [],
    currentOrder: null,
    orderHistory: [],

    createOrder: async (orderData) => {
      const order = await orderService.createOrder(
        orderData
      );
      set((state) => ({
        orders: [order, ...state.orders],
        currentOrder: order,
      }));

      // 注文作成イベント発行
      eventBus.emit('order:created', { order });

      return order;
    },

    trackOrder: async (orderId) => {
      const tracking = await orderService.getTrackingInfo(
        orderId
      );
      set((state) => ({
        orders: state.orders.map((order) =>
          order.id === orderId
            ? { ...order, tracking }
            : order
        ),
      }));
    },

    fetchOrderHistory: async (userId) => {
      const history = await orderService.getOrderHistory(
        userId
      );
      set({ orderHistory: history });
    },
  })
);

マルチテナントアプリでの分割戦略

マルチテナント環境では、テナントごとに状態を分離する必要があります:

typescript// テナント管理ストア
export const useTenantStore = create((set, get) => ({
  currentTenant: null,
  availableTenants: [],
  tenantSettings: {},

  switchTenant: async (tenantId) => {
    const tenant = await tenantService.getTenant(tenantId);
    const settings = await tenantService.getSettings(
      tenantId
    );

    set({
      currentTenant: tenant,
      tenantSettings: settings,
    });

    // テナント切り替えイベント発行
    eventBus.emit('tenant:switched', { tenant, settings });
  },
}));

// テナント固有データストア作成関数
const createTenantSpecificStore = <T>(
  storeFactory: () => T
) => {
  const stores = new Map<string, any>();

  return {
    getStore: (tenantId: string) => {
      if (!stores.has(tenantId)) {
        stores.set(tenantId, create(storeFactory));
      }
      return stores.get(tenantId);
    },

    clearTenantStore: (tenantId: string) => {
      stores.delete(tenantId);
    },

    getCurrentTenantStore: () => {
      const currentTenant =
        useTenantStore.getState().currentTenant;
      if (!currentTenant)
        throw new Error('No current tenant');
      return stores.get(currentTenant.id);
    },
  };
};

// 使用例
export const tenantUserStores = createTenantSpecificStore(
  () => ({
    users: [],
    roles: [],
    permissions: [],

    fetchUsers: async (tenantId) => {
      const users = await userService.getUsersByTenant(
        tenantId
      );
      return { users };
    },
  })
);

export const tenantDataStores = createTenantSpecificStore(
  () => ({
    data: [],
    metrics: {},

    fetchData: async (tenantId) => {
      const data = await dataService.getDataByTenant(
        tenantId
      );
      return { data };
    },
  })
);

リアルタイムアプリでの状態同期

WebSocket を使用したリアルタイムアプリケーションでの同期戦略:

typescript// リアルタイム同期ストア
export const useRealtimeSyncStore = create((set, get) => ({
  connections: new Map(),
  syncStatus: {},

  establishConnection: (namespace, options = {}) => {
    const socket = io(`/${namespace}`, options);
    const { connections } = get();

    connections.set(namespace, socket);
    set({ connections: new Map(connections) });

    // 接続状態の監視
    socket.on('connect', () => {
      set((state) => ({
        syncStatus: {
          ...state.syncStatus,
          [namespace]: 'connected',
        },
      }));
    });

    socket.on('disconnect', () => {
      set((state) => ({
        syncStatus: {
          ...state.syncStatus,
          [namespace]: 'disconnected',
        },
      }));
    });

    return socket;
  },

  subscribeToUpdates: (namespace, eventName, callback) => {
    const { connections } = get();
    const socket = connections.get(namespace);

    if (socket) {
      socket.on(eventName, callback);
    }
  },
}));

// チャットルームストア(リアルタイム同期対応)
export const useChatRoomStore = create((set, get) => {
  // WebSocket接続を確立
  const socket = useRealtimeSyncStore
    .getState()
    .establishConnection('chat');

  // リアルタイム更新の購読
  useRealtimeSyncStore
    .getState()
    .subscribeToUpdates(
      'chat',
      'message:received',
      (message) => {
        set((state) => ({
          messages: [...state.messages, message],
        }));
      }
    );

  useRealtimeSyncStore
    .getState()
    .subscribeToUpdates('chat', 'user:joined', (user) => {
      set((state) => ({
        participants: [...state.participants, user],
      }));
    });

  return {
    messages: [],
    participants: [],
    currentRoom: null,

    joinRoom: (roomId) => {
      socket.emit('room:join', { roomId });
      set({ currentRoom: roomId });
    },

    sendMessage: (content) => {
      const message = {
        content,
        timestamp: Date.now(),
        sender: useUserStore.getState().user,
      };

      socket.emit('message:send', message);

      // 楽観的更新
      set((state) => ({
        messages: [
          ...state.messages,
          { ...message, pending: true },
        ],
      }));
    },
  };
});

まとめ

大規模な Web アプリケーションにおける Zustand の分割ストア設計は、アプリケーションの品質と開発効率を大きく左右する重要な要素です。適切に設計された分割ストアは、コードの保守性、パフォーマンス、テスト容易性を大幅に向上させます。

ドメイン駆動設計の原則に基づいたストア分割により、ビジネスロジックの責任範囲を明確にし、チーム開発での混乱を避けることができます。また、レイヤーアーキテクチャの適用により、技術的関心事とビジネス関心事を適切に分離できます。

ストア間の通信には、イベント駆動アーキテクチャや発行/購読パターンなど、疎結合を実現するパターンを活用することが重要です。これにより、個々のストアの独立性を保ちながら、必要な連携を実現できます。

Zustand の柔軟性は、これらのアーキテクチャパターンを実装する上で大きな利点となります。軽量でありながら強力な機能を提供する Zustand を活用して、スケーラブルで保守しやすいアプリケーションを構築していきましょう。

適切な分割ストア設計は一朝一夕に身につくものではありませんが、継続的な改善とリファクタリングを通じて、より良いアーキテクチャを目指すことが重要です。

関連リンク