T-CREATOR

Zustand の subscribe メソッド活用術:外部システムとの連携を強化

Zustand の subscribe メソッド活用術:外部システムとの連携を強化

Zustand の subscribe メソッドは、状態管理を超えた可能性を秘めています。この記事では、外部システムとの連携において subscribe メソッドがどのように革命的な解決策を提供するのかを詳しく解説していきます。

従来の状態管理では、状態の変更を外部システムに伝えるために手動でイベントを発火させたり、定期的なポーリングを行ったりする必要がありました。しかし、subscribe メソッドを活用することで、状態の変更を自動的に検知し、リアルタイムで外部システムと連携できるようになります。

この記事を読むことで、あなたのアプリケーションは単なる状態管理から、外部システムとシームレスに連携する高度なシステムへと進化するでしょう。

subscribe メソッドの基本理解

subscribe メソッドとは

Zustand の subscribe メソッドは、ストアの状態変更を監視するための強力なツールです。状態が変更されるたびに、指定したコールバック関数が自動的に実行されます。

typescript// 基本的な subscribe メソッドの使用例
import { create } from 'zustand';

interface UserStore {
  user: { id: string; name: string } | null;
  setUser: (user: { id: string; name: string }) => void;
}

const useUserStore = create<UserStore>((set) => ({
  user: null,
  setUser: (user) => set({ user }),
}));

// subscribe メソッドで状態変更を監視
const unsubscribe = useUserStore.subscribe(
  (state) => state.user,
  (user) => {
    console.log('ユーザー情報が変更されました:', user);
  }
);

このコードでは、user 状態が変更されるたびにコンソールにログが出力されます。subscribe メソッドは、状態の変更を自動的に検知し、リアクティブな処理を可能にします。

従来の状態管理との違い

従来の状態管理では、状態の変更を外部システムに伝えるために以下のような方法を使用していました:

typescript// 従来の方法:手動でイベントを発火
const useUserStore = create<UserStore>((set, get) => ({
  user: null,
  setUser: (user) => {
    set({ user });
    // 手動で外部システムに通知
    notifyExternalSystem(user);
  },
}));

function notifyExternalSystem(user: {
  id: string;
  name: string;
}) {
  // 外部システムへの通知処理
  console.log('外部システムに通知:', user);
}

この方法では、状態を更新するたびに手動で外部システムへの通知処理を記述する必要があります。しかし、subscribe メソッドを使用することで、この処理を自動化できます。

typescript// subscribe メソッドを使用した方法
const useUserStore = create<UserStore>((set) => ({
  user: null,
  setUser: (user) => set({ user }),
}));

// 状態変更を自動的に監視
useUserStore.subscribe(
  (state) => state.user,
  (user) => {
    if (user) {
      notifyExternalSystem(user);
    }
  }
);

この方法では、状態の更新ロジックと外部システムへの通知ロジックが分離され、コードの保守性が向上します。

リアクティブな状態監視の仕組み

subscribe メソッドの内部では、状態の変更を効率的に検知するための仕組みが働いています。

typescript// subscribe メソッドの内部動作を理解するための例
const useCounterStore = create<{
  count: number;
  increment: () => void;
  decrement: () => void;
}>((set) => ({
  count: 0,
  increment: () =>
    set((state) => ({ count: state.count + 1 })),
  decrement: () =>
    set((state) => ({ count: state.count - 1 })),
}));

// 複数の状態を監視
const unsubscribe1 = useCounterStore.subscribe(
  (state) => state.count,
  (count) => {
    console.log('カウントが変更されました:', count);
  }
);

const unsubscribe2 = useCounterStore.subscribe(
  (state) => ({
    count: state.count,
    isPositive: state.count > 0,
  }),
  (newState) => {
    console.log('状態の詳細:', newState);
  }
);

subscribe メソッドは、指定されたセレクター関数が返す値が前回と異なる場合にのみコールバックを実行します。これにより、不要な処理を避け、パフォーマンスを最適化しています。

外部システム連携の課題

状態変更の検知が困難

現代の Web アプリケーションでは、複数の外部システムと連携する必要があります。しかし、状態の変更を適切に検知し、外部システムに通知することは容易ではありません。

typescript// 問題のある実装例
const useOrderStore = create<{
  orders: Order[];
  addOrder: (order: Order) => void;
}>((set) => ({
  orders: [],
  addOrder: (order) =>
    set((state) => ({
      orders: [...state.orders, order],
    })),
}));

// 外部システムへの通知が漏れがち
const addNewOrder = (order: Order) => {
  useOrderStore.getState().addOrder(order);
  // 通知処理を忘れがち
  // notifyInventorySystem(order)
  // notifyPaymentSystem(order)
};

このような実装では、開発者が手動で通知処理を記述する必要があり、忘れる可能性があります。

リアルタイム同期の実現

外部システムとのリアルタイム同期を実現するためには、状態の変更を即座に検知し、適切なタイミングで外部システムに通知する必要があります。

typescript// リアルタイム同期が困難な例
const useChatStore = create<{
  messages: Message[];
  addMessage: (message: Message) => void;
}>((set) => ({
  messages: [],
  addMessage: (message) =>
    set((state) => ({
      messages: [...state.messages, message],
    })),
}));

// 定期的なポーリング(非効率)
setInterval(() => {
  const messages = useChatStore.getState().messages;
  // 新しいメッセージがあるかチェック
  // 外部システムに送信
}, 1000);

この方法では、1 秒ごとにポーリングを行うため、リアルタイム性が低く、サーバーリソースも無駄に消費してしまいます。

パフォーマンスの最適化

外部システムとの連携において、パフォーマンスは重要な課題です。不適切な実装では、アプリケーションの応答性が低下する可能性があります。

typescript// パフォーマンスの問題がある実装
const useProductStore = create<{
  products: Product[];
  updateProduct: (
    id: string,
    updates: Partial<Product>
  ) => void;
}>((set) => ({
  products: [],
  updateProduct: (id, updates) =>
    set((state) => ({
      products: state.products.map((product) =>
        product.id === id
          ? { ...product, ...updates }
          : product
      ),
    })),
}));

// 毎回外部APIを呼び出す(非効率)
const updateProduct = (
  id: string,
  updates: Partial<Product>
) => {
  useProductStore.getState().updateProduct(id, updates);

  // 状態が変更されるたびにAPIを呼び出し
  fetch(`/api/products/${id}`, {
    method: 'PATCH',
    body: JSON.stringify(updates),
  });
};

この実装では、状態が変更されるたびに外部 API を呼び出すため、ネットワークリクエストが増加し、パフォーマンスが低下します。

subscribe メソッドによる解決策

状態変更の自動検知

subscribe メソッドを使用することで、状態の変更を自動的に検知し、適切なタイミングで外部システムに通知できます。

typescript// subscribe メソッドによる自動検知
const useOrderStore = create<{
  orders: Order[];
  addOrder: (order: Order) => void;
  updateOrder: (
    id: string,
    updates: Partial<Order>
  ) => void;
}>((set) => ({
  orders: [],
  addOrder: (order) =>
    set((state) => ({
      orders: [...state.orders, order],
    })),
  updateOrder: (id, updates) =>
    set((state) => ({
      orders: state.orders.map((order) =>
        order.id === id ? { ...order, ...updates } : order
      ),
    })),
}));

// 状態変更を自動的に監視
useOrderStore.subscribe(
  (state) => state.orders,
  (orders) => {
    // 最新の注文を取得
    const latestOrder = orders[orders.length - 1];
    if (latestOrder) {
      // 外部システムに自動通知
      notifyInventorySystem(latestOrder);
      notifyPaymentSystem(latestOrder);
    }
  }
);

この実装では、注文が追加されるたびに自動的に外部システムに通知されます。開発者が手動で通知処理を記述する必要がありません。

コールバック関数の活用

subscribe メソッドでは、コールバック関数を活用して複雑な処理を実装できます。

typescript// コールバック関数を活用した実装
const useNotificationStore = create<{
  notifications: Notification[];
  addNotification: (notification: Notification) => void;
  markAsRead: (id: string) => void;
}>((set) => ({
  notifications: [],
  addNotification: (notification) =>
    set((state) => ({
      notifications: [...state.notifications, notification],
    })),
  markAsRead: (id) =>
    set((state) => ({
      notifications: state.notifications.map((notif) =>
        notif.id === id ? { ...notif, read: true } : notif
      ),
    })),
}));

// 複雑なコールバック処理
useNotificationStore.subscribe(
  (state) => state.notifications,
  (notifications) => {
    const unreadCount = notifications.filter(
      (n) => !n.read
    ).length;

    // 未読通知数に基づいて処理を実行
    if (unreadCount > 0) {
      // ブラウザ通知を表示
      if (Notification.permission === 'granted') {
        new Notification(
          `未読通知が${unreadCount}件あります`
        );
      }

      // 外部システムに未読数を送信
      updateUnreadCount(unreadCount);
    }
  }
);

この実装では、通知の状態変更に基づいて、ブラウザ通知の表示や外部システムへの送信など、複数の処理を自動的に実行します。

メモリリークの防止

subscribe メソッドを使用する際は、適切にアンサブスクライブすることでメモリリークを防ぐ必要があります。

typescript// メモリリークを防ぐ実装
import { useEffect } from 'react';

const useOrderSubscription = () => {
  useEffect(() => {
    // サブスクライブ
    const unsubscribe = useOrderStore.subscribe(
      (state) => state.orders,
      (orders) => {
        console.log('注文が更新されました:', orders);
      }
    );

    // コンポーネントのアンマウント時にアンサブスクライブ
    return () => {
      unsubscribe();
    };
  }, []);
};

// カスタムフックとして実装
const useStoreSubscription = <T>(
  store: any,
  selector: (state: any) => T,
  callback: (value: T) => void
) => {
  useEffect(() => {
    const unsubscribe = store.subscribe(selector, callback);
    return unsubscribe;
  }, [store, selector, callback]);
};

この実装では、React コンポーネントのライフサイクルに合わせてサブスクリプションを管理し、メモリリークを防いでいます。

具体的な実装例

WebSocket との連携

WebSocket を使用したリアルタイム通信において、subscribe メソッドは非常に有用です。

typescript// WebSocket との連携例
const useChatStore = create<{
  messages: Message[];
  addMessage: (message: Message) => void;
  setMessages: (messages: Message[]) => void;
}>((set) => ({
  messages: [],
  addMessage: (message) =>
    set((state) => ({
      messages: [...state.messages, message],
    })),
  setMessages: (messages) => set({ messages }),
}));

// WebSocket 接続の管理
class ChatWebSocket {
  private ws: WebSocket | null = null;
  private unsubscribe: (() => void) | null = null;

  connect() {
    this.ws = new WebSocket('wss://api.example.com/chat');

    // メッセージ受信時の処理
    this.ws.onmessage = (event) => {
      const message = JSON.parse(event.data);
      useChatStore.getState().addMessage(message);
    };

    // 状態変更を監視してWebSocketに送信
    this.unsubscribe = useChatStore.subscribe(
      (state) => state.messages,
      (messages) => {
        const latestMessage = messages[messages.length - 1];
        if (
          latestMessage &&
          this.ws?.readyState === WebSocket.OPEN
        ) {
          this.ws.send(JSON.stringify(latestMessage));
        }
      }
    );
  }

  disconnect() {
    if (this.unsubscribe) {
      this.unsubscribe();
    }
    if (this.ws) {
      this.ws.close();
    }
  }
}

この実装では、チャットメッセージの状態変更を自動的に検知し、WebSocket を通じてリアルタイムで送信します。

ローカルストレージとの同期

ローカルストレージとの同期においても、subscribe メソッドが活躍します。

typescript// ローカルストレージとの同期例
const useSettingsStore = create<{
  theme: 'light' | 'dark';
  language: string;
  notifications: boolean;
  setTheme: (theme: 'light' | 'dark') => void;
  setLanguage: (language: string) => void;
  setNotifications: (enabled: boolean) => void;
}>((set) => ({
  theme: 'light',
  language: 'ja',
  notifications: true,
  setTheme: (theme) => set({ theme }),
  setLanguage: (language) => set({ language }),
  setNotifications: (enabled) =>
    set({ notifications: enabled }),
}));

// ローカルストレージとの自動同期
useSettingsStore.subscribe(
  (state) => state,
  (settings) => {
    try {
      localStorage.setItem(
        'app-settings',
        JSON.stringify(settings)
      );
    } catch (error) {
      console.error(
        'ローカルストレージへの保存に失敗しました:',
        error
      );
    }
  }
);

// 初期化時にローカルストレージから読み込み
const initializeSettings = () => {
  try {
    const savedSettings =
      localStorage.getItem('app-settings');
    if (savedSettings) {
      const settings = JSON.parse(savedSettings);
      useSettingsStore.setState(settings);
    }
  } catch (error) {
    console.error(
      'ローカルストレージからの読み込みに失敗しました:',
      error
    );
  }
};

この実装では、設定の変更を自動的にローカルストレージに保存し、アプリケーション起動時に復元します。

外部 API との通信

外部 API との通信においても、subscribe メソッドを使用して効率的な実装が可能です。

typescript// 外部APIとの通信例
const useUserStore = create<{
  user: User | null;
  setUser: (user: User) => void;
  updateUser: (updates: Partial<User>) => void;
}>((set) => ({
  user: null,
  setUser: (user) => set({ user }),
  updateUser: (updates) =>
    set((state) => ({
      user: state.user
        ? { ...state.user, ...updates }
        : null,
    })),
}));

// デバウンス処理付きのAPI通信
let updateTimeout: NodeJS.Timeout | null = null;

useUserStore.subscribe(
  (state) => state.user,
  (user) => {
    if (user) {
      // 前のタイマーをクリア
      if (updateTimeout) {
        clearTimeout(updateTimeout);
      }

      // 500ms後にAPIを呼び出し
      updateTimeout = setTimeout(async () => {
        try {
          await fetch(`/api/users/${user.id}`, {
            method: 'PATCH',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(user),
          });
        } catch (error) {
          console.error(
            'ユーザー情報の更新に失敗しました:',
            error
          );
        }
      }, 500);
    }
  }
);

この実装では、ユーザー情報の変更を検知し、デバウンス処理を適用して API を呼び出します。これにより、頻繁な API 呼び出しを防ぎ、パフォーマンスを最適化しています。

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

選択的サブスクライブ

すべての状態変更を監視するのではなく、必要な部分のみを選択的にサブスクライブすることで、パフォーマンスを向上させることができます。

typescript// 選択的サブスクライブの例
const useProductStore = create<{
  products: Product[];
  selectedProduct: Product | null;
  filters: FilterOptions;
  addProduct: (product: Product) => void;
  selectProduct: (product: Product) => void;
  updateFilters: (filters: FilterOptions) => void;
}>((set) => ({
  products: [],
  selectedProduct: null,
  filters: { category: '', price: 0 },
  addProduct: (product) =>
    set((state) => ({
      products: [...state.products, product],
    })),
  selectProduct: (product) =>
    set({ selectedProduct: product }),
  updateFilters: (filters) => set({ filters }),
}));

// 特定の状態のみを監視
useProductStore.subscribe(
  (state) => state.selectedProduct,
  (selectedProduct) => {
    if (selectedProduct) {
      // 選択された商品の詳細を取得
      fetchProductDetails(selectedProduct.id);
    }
  }
);

// 別の状態を監視
useProductStore.subscribe(
  (state) => state.filters,
  (filters) => {
    // フィルター変更時に検索を実行
    searchProducts(filters);
  }
);

この実装では、選択された商品とフィルターの変更を個別に監視し、それぞれに適した処理を実行します。

デバウンス処理

頻繁に発生する状態変更に対して、デバウンス処理を適用することで、パフォーマンスを最適化できます。

typescript// デバウンス処理の実装
const createDebouncedSubscription = <T>(
  store: any,
  selector: (state: any) => T,
  callback: (value: T) => void,
  delay: number = 300
) => {
  let timeoutId: NodeJS.Timeout | null = null;

  return store.subscribe(selector, (value: T) => {
    if (timeoutId) {
      clearTimeout(timeoutId);
    }

    timeoutId = setTimeout(() => {
      callback(value);
    }, delay);
  });
};

// デバウンス処理を使用した例
const useSearchStore = create<{
  query: string;
  setQuery: (query: string) => void;
}>((set) => ({
  query: '',
  setQuery: (query) => set({ query }),
}));

// 検索クエリの変更をデバウンス
createDebouncedSubscription(
  useSearchStore,
  (state) => state.query,
  (query) => {
    if (query.length > 0) {
      performSearch(query);
    }
  },
  500
);

この実装では、検索クエリの変更を 500ms 間デバウンスし、ユーザーが入力を終えてから検索を実行します。

メモ化との組み合わせ

React のメモ化機能と組み合わせることで、さらなるパフォーマンス最適化が可能です。

typescript// メモ化との組み合わせ例
import { useMemo, useCallback } from 'react'

const useOptimizedSubscription = <T>(
  store: any,
  selector: (state: any) => T,
  callback: (value: T) => void
) => {
  // セレクターをメモ化
  const memoizedSelector = useCallback(selector, [])

  // コールバックをメモ化
  const memoizedCallback = useCallback(callback, [])

  useEffect(() => {
    const unsubscribe = store.subscribe(memoizedSelector, memoizedCallback)
    return unsubscribe
  }, [store, memoizedSelector, memoizedCallback])
}

// 使用例
const ProductList = () => {
  const products = useProductStore((state) => state.products)

  // 高価な計算をメモ化
  const expensiveCalculation = useMemo(() => {
    return products.filter(p => p.price > 100).map(p => ({
      ...p,
      discountedPrice: p.price * 0.9
    }))
  }, [products])

  // 最適化されたサブスクリプション
  useOptimizedSubscription(
    useProductStore,
    (state) => state.products,
    (products) => {
      console.log('商品リストが更新されました:', products.length)
    }
  )

  return (
    <div>
      {expensiveCalculation.map(product => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  )
}

この実装では、セレクターとコールバックをメモ化し、不要な再計算を防いでいます。

まとめ

Zustand の subscribe メソッドは、単なる状態管理を超えた可能性を秘めています。外部システムとの連携において、このメソッドを活用することで、以下のようなメリットを得ることができます。

自動化による開発効率の向上 状態の変更を自動的に検知し、外部システムへの通知を自動化することで、開発者は手動でイベントを管理する必要がなくなります。これにより、コードの保守性が向上し、バグの発生リスクも軽減されます。

リアルタイム性の実現 subscribe メソッドを使用することで、状態の変更を即座に検知し、リアルタイムで外部システムと連携できます。これにより、ユーザーエクスペリエンスが大幅に向上します。

パフォーマンスの最適化 選択的サブスクライブやデバウンス処理を活用することで、不要な処理を避け、アプリケーションのパフォーマンスを最適化できます。

拡張性の確保 subscribe メソッドは、新しい外部システムとの連携を簡単に追加できる柔軟なアーキテクチャを提供します。これにより、アプリケーションの成長に合わせて機能を拡張できます。

この記事で紹介したテクニックを活用することで、あなたのアプリケーションは単なる状態管理から、外部システムとシームレスに連携する高度なシステムへと進化するでしょう。subscribe メソッドの可能性を最大限に活用し、ユーザーにとって価値のあるアプリケーションを構築してください。

関連リンク