🦔

TanStack Queryを活用すればポーリング処理を簡単に実装できる

2024/12/03に公開

Reactで、サーバーに定期的にリクエストを送りデータ更新を行うというような処理を実装したことはありますか?useEffectやsetIntervalを組み合わせて実装することもできますが、以下のような点を考慮する必要があるため、コードが複雑になりやすいです。

  • コンポーネントのライフサイクル
  • クリーンアップ処理
  • エラーハンドリング

本記事では、TanStack Queryを使って、これらの課題をクリーンに解決する実装パターンを紹介します。

ケース1: ユーザー会員証トークンの自動更新

ユーザーの会員証QRコードを表示するケースを考えましょう。どのようなアプリでも、以下のような要件が必要になってくるかと思います。

  • セキュリティ要件
    • QRコードの不正コピー防止のため、QRコードの元になっているトークンの有効期限を5分などに設定して、定期的に更新したい
  • UX要件
    • ユーザーが会員証画面を閲覧している間は常に有効なQRコードを表示
    • 会員証画面を見ていない間は更新処理を停止
    • 会員証画面を再度閲覧したときは最新のQRコードを表示

      会員証の例

実装例

const MEMBERSHIP_TOKEN_KEY = 'membership-token';

export const useMembershipToken = () => {
  const { status, userId } = useUserAuthStatus();
  const { showDialog } = useDialog();
  
  const { data: membershipToken, isLoading } = useQuery({
    queryKey: [MEMBERSHIP_TOKEN_KEY],
    queryFn: async () => {
      return await getMembershipToken(userId);
    },
    // ログイン時だけ実行
    enabled: status === 'login',
    refetchInterval: 5 * 60 * 1000, // 5分
    refetchIntervalInBackground: false,
    refetchOnWindowFocus: true,
    retry: false,
    onError: () => {
      showDialog({
        title: '会員情報の取得に失敗しました',
        description: 'しばらく時間をおいてから再度お試しください',
      })
    },
  });
  
  return {
    isLoading,
    membershipToken
  };
}

// hooksの呼び出し側
export const MemberShipQrCode = () => {
  const { isLoading, membershipToken } = useMembershipToken();
    
  if(isLoading) {
    return (<LoadingSpinner />);
  }

  return (
    <QrCode membershipToken={membershipToken}/>
  );
}

ポイント

  1. refetchIntervalを活用して、定期実行処理を簡潔に実装

    • refetchInterval: 5 * 60 * 1000
      • 「5分おきにデータを更新する」という条件を1文で記述可能です。
  2. 簡潔なバックグラウンド更新の制御

    • refetchIntervalInBackground: false
      • 会員証画面が閲覧されているときのみ更新
    • refetchOnWindowFocus: true
      • 会員証画面を再閲覧した際にに最新状態を確保

    また、この書き方はwebアプリに限らずネイティブアプリでも使えます。Universal Appを開発している方々にとって、特に便利だと思います。

  3. onErrorを使ったエラーハンドリング

    トークン取得処理が失敗した場合は、ダイアログを表示してユーザーに通知します。このような処理もonErrorを使えば簡単に書くことができます。

ケース2: 注文ステータス確認ポーリング

Stripeのような外部決済サービスを利用して、以下のような商品注文フローを構築しているケースを考えています。

1. 決済開始フェーズ

  • ユーザーが「購入する」ボタンをクリック
  • フロントエンドから自社バックエンドサーバーへ決済開始のAPIリクエストを送信
  • 自社バックエンドサーバー側で自社データベース内に商品注文データを作成する。
  • 外部決済サービスを利用した決済セッション作成を行う(Stripeを使っている場合は、Payment Intentの作成などを行う)
  • この時点では注文ステータスは「PENDING」

2. ユーザー認証フェーズ

  • クレカ決済の場合、3Dセキュアを実施する。
  • ユーザーが認証情報を入力
  • 認証結果は外部決済サービスで処理

3. 決済完了フェーズ

  • 認証成功時、決済サービスから自社バックエンドサーバーへWebhookを送る
  • 自社バックエンドサーバー側では、自社データベース上の注文ステータスを「PENDING」→「SUCCESS」に更新

4. 完了確認フェーズ

  • フロントエンドは自社バックエンドサーバーに対して注文ステータスを定期的にポーリング(1秒間隔)
  • 注文ステータスが「SUCCESS」に変わったことを検知したら完了画面へ遷移
  • ユーザーへ「注文が完了しました」と表示する

実装例

// 以下の3つのステータスがあるとしましょう。
type OrderStatus = 'PENDING' | 'SUCCESS' | 'FAILED'

const ORDER_STATUS_KEY = 'order-status'

// 注文ステータスをポーリングするhooks
export const useOrderStatusPolling = (orderId: string) => {
  // `data`の初期値はundefined
  // queryFn内の処理でreturnされる値が入る
  // queryFn内の処理で例外がスローされた場合はundefinedのまま
  const { data, isLoading } = useQuery({
    queryKey: [ORDER_STATUS_KEY, orderId],
    queryFn: async () => {
      // 自社バックエンドサーバーへ問い合わせて注文ステータスを取得
      const orderStatus: OrderStatus = await getOrderStatus();
      // ステータスがPENDINGの場合は、例外をスローして再度リクエストを送る
      if (orderStatus === 'PENDING') throw new Error();
      
      return orderStatus;
    },
    retry: 10,
    retryDelay: 1000,
  });
  
  return {
    isLoading,
    isSuccessful: data === 'SUCCESS'
  };
}

// hooksの呼び出し側
export const Page = () => {
  const { isLoading, isSuccessful } = useOrderStatusPolling(orderId);

  if(isLoading) {
    return (
      <>
        <Text>決済処理中です。しばらくお待ちください</Text>
        <LoadingSpinner />
      </>
    );
  }

  return (
    <Text>{isSuccessful ? "注文が完了しました" : "決済処理が失敗しました"}<Text/>
  );
}

ポイント

  1. 「例外スロー時の再試行」を活用したシンプルな実装

    TanStack Queryの仕様で、queryFn内で例外がスローされるとqueryFn内の処理が再実行されます。この仕様を利用して、ステータスが「PENDING」の場合は意図的にエラーをスローすることで、ポーリング処理をシンプルに実装しています。

  2. 適切なポーリング間隔の設定

    • retry: 10で最大10回までリトライ(約10秒)を許容
    • retryDelay: 1000で1秒間隔のポーリングを実現

まとめ

TanStack Queryを活用することで、複雑になりがちなポーリング処理を簡単に実装できます。様々なユースケースに応用可能だと思うので、ぜひ参考にしてみてください。

TrustHub テックブログ

Discussion