TanStack Queryを活用すればポーリング処理を簡単に実装できる
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}/>
);
}
ポイント
-
refetchInterval
を活用して、定期実行処理を簡潔に実装-
refetchInterval: 5 * 60 * 1000
- 「5分おきにデータを更新する」という条件を1文で記述可能です。
-
-
簡潔なバックグラウンド更新の制御
-
refetchIntervalInBackground: false
- 会員証画面が閲覧されているときのみ更新
-
refetchOnWindowFocus: true
- 会員証画面を再閲覧した際にに最新状態を確保
また、この書き方はwebアプリに限らずネイティブアプリでも使えます。Universal Appを開発している方々にとって、特に便利だと思います。
-
-
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/>
);
}
ポイント
-
「例外スロー時の再試行」を活用したシンプルな実装
TanStack Queryの仕様で、
queryFn
内で例外がスローされるとqueryFn
内の処理が再実行されます。この仕様を利用して、ステータスが「PENDING」の場合は意図的にエラーをスローすることで、ポーリング処理をシンプルに実装しています。 -
適切なポーリング間隔の設定
-
retry: 10
で最大10回までリトライ(約10秒)を許容 -
retryDelay: 1000
で1秒間隔のポーリングを実現
-
まとめ
TanStack Queryを活用することで、複雑になりがちなポーリング処理を簡単に実装できます。様々なユースケースに応用可能だと思うので、ぜひ参考にしてみてください。
Discussion