😊
React + SWRで手動ポーリングを簡潔にリファクタリングした話
背景
Webアプリケーションでは、サーバー側で実行される時間のかかる処理(ファイル変換、データ処理、レポート生成など)の進捗をリアルタイムで取得したい場面がよくあります。
従来、私たちのプロジェクトではsetIntervalとuseEffectを組み合わせた手動ポーリングで非同期ジョブの状態を監視していましたが、コードが複雑化し、メモリリークのリスクもありました。
そこで、既にプロジェクトで使用していたSWRのrefreshIntervalオプションを活用してリファクタリングを行い、大幅にコードを簡潔化できたので紹介します。
Before: 手動ポーリングの実装
// Before: 手動でsetInterval/clearIntervalを管理
import { useEffect, useRef, useState } from "react";
const AsyncJobMonitor = () => {
const [jobId, setJobId] = useState<string | null>(null);
const [jobStatus, setJobStatus] = useState<string>("");
const [jobResult, setJobResult] = useState<string>("");
const [loading, setLoading] = useState(false);
const pollingIntervalRef = useRef<NodeJS.Timeout | null>(null);
// 複雑なポーリングロジック
useEffect(() => {
if (!jobId) return;
const pollJobStatus = async () => {
try {
const response = await fetchJobStatus(jobId);
if (response.result !== undefined) {
setJobResult(response.result);
}
if (response.status === "SUCCESS") {
setJobStatus("completed");
setLoading(false);
// ポーリング停止
if (pollingIntervalRef.current) {
clearInterval(pollingIntervalRef.current);
pollingIntervalRef.current = null;
}
} else if (response.status === "FAILED") {
setJobStatus("failed");
setLoading(false);
// ポーリング停止
if (pollingIntervalRef.current) {
clearInterval(pollingIntervalRef.current);
pollingIntervalRef.current = null;
}
alert("処理が失敗しました");
}
} catch (error) {
console.error("ジョブ状態取得エラー:", error);
setLoading(false);
if (pollingIntervalRef.current) {
clearInterval(pollingIntervalRef.current);
pollingIntervalRef.current = null;
}
alert("エラーが発生しました");
}
};
// 既存のポーリングをクリア
if (pollingIntervalRef.current) {
clearInterval(pollingIntervalRef.current);
}
// 1秒間隔でポーリング開始
pollingIntervalRef.current = setInterval(pollJobStatus, 1000);
// 初回実行
pollJobStatus();
}, [jobId]);
// クリーンアップ
useEffect(() => {
return () => {
if (pollingIntervalRef.current) {
clearInterval(pollingIntervalRef.current);
}
};
}, []);
const startJob = async () => {
setLoading(true);
const response = await executeAsyncJob();
setJobId(response.jobId);
};
return (
<div>
<button onClick={startJob} disabled={loading}>
{loading ? "処理中..." : "ジョブ開始"}
</button>
{jobStatus === "completed" && <div>処理完了: {jobResult}</div>}
{jobStatus === "failed" && <div>処理失敗: {jobResult}</div>}
</div>
);
};
After: SWRを使った簡潔な実装
// After: SWRのrefreshIntervalを活用
import useSWR from "swr";
import { useState } from "react";
const AsyncJobMonitor = () => {
const [jobId, setJobId] = useState<string | null>(null);
const [jobStatus, setJobStatus] = useState<string>("");
const [jobResult, setJobResult] = useState<string>("");
const [loading, setLoading] = useState(false);
const [shouldPoll, setShouldPoll] = useState(true);
// SWRによる自動ポーリング
useSWR(
jobId ? `job-${jobId}` : null,
async () => {
if (!jobId) return null;
const response = await fetchJobStatus(jobId);
return response;
},
{
refreshInterval: shouldPoll ? 1000 : 0, // 1秒間隔でポーリング
revalidateOnFocus: false,
revalidateOnReconnect: true,
onSuccess: (data) => {
if (!data) return;
// 結果を設定
if (data.result !== undefined) {
setJobResult(data.result);
}
if (data.status === "SUCCESS") {
setJobStatus("completed");
setLoading(false);
setShouldPoll(false); // ポーリング停止
} else if (data.status === "FAILED") {
setJobStatus("failed");
setLoading(false);
setShouldPoll(false); // ポーリング停止
alert("処理が失敗しました");
}
},
onError: (error) => {
console.error("ジョブ状態取得エラー:", error);
setLoading(false);
setShouldPoll(false);
alert("エラーが発生しました");
}
}
);
const startJob = async () => {
setLoading(true);
setShouldPoll(true);
const response = await executeAsyncJob();
setJobId(response.jobId);
};
return (
<div>
<button onClick={startJob} disabled={loading}>
{loading ? "処理中..." : "ジョブ開始"}
</button>
{jobStatus === "completed" && <div>処理完了: {jobResult}</div>}
{jobStatus === "failed" && <div>処理失敗: {jobResult}</div>}
</div>
);
};
改善のポイント
1. コード量の大幅削減
- Before: 約50行のポーリングロジック
- After: 約30行で同等の機能を実現
2. メモリリークのリスク軽減
- 手動での
setInterval/clearInterval管理が不要 - SWRが自動的にクリーンアップを実行
- コンポーネントのアンマウント時も安全
3. 宣言的なエラーハンドリング
// Before: try-catch内で複雑な状態管理
try {
// ポーリング処理
} catch (error) {
// エラー時のクリーンアップを手動で実行
}
// After: onErrorコールバックで宣言的に処理
{
onError: (error) => {
// SWRが自動的にポーリングを停止
handleError(error);
}
}
4. 条件付きポーリングの簡潔な実装
// refreshIntervalで動的にポーリング間隔を制御
refreshInterval: shouldPoll ? 1000 : 0
5. 重複リクエストの防止
{
dedupingInterval: 500, // 500ms以内の重複リクエストを自動で除外
revalidateOnFocus: false, // フォーカス時の自動再取得を無効化
}
SWR活用のメリット
1. 自動リソース管理
- ポーリングの開始/停止をSWRが自動制御
- メモリリークやゾンビタイマーのリスクを軽減
2. 豊富なオプション
-
refreshInterval: 動的なポーリング間隔制御 -
revalidateOnFocus/revalidateOnReconnect: 再検証タイミングの細かい制御 -
dedupingInterval: 重複リクエストの自動排除
3. 宣言的な記述
-
onSuccess/onErrorコールバックで状態変更を宣言的に記述 - 副作用の管理が簡潔
4. キャッシュ機能
- 同一キーでの重複リクエストを自動で最適化
- ネットワーク効率の向上
まとめ
手動ポーリング処理をSWRにリファクタリングすることで:
- 可読性向上: 複雑な状態管理ロジックが大幅に簡潔化
- 保守性向上: メモリリークリスクの軽減、自動リソース管理
- 開発効率向上: 宣言的な記述でバグの混入リスクを軽減
既にSWRを導入しているプロジェクトであれば、追加の依存関係なしで恩恵を受けられます。非同期処理の監視が必要な場面では、SWRのrefreshIntervalオプションを積極的に活用することをおすすめします。
この事例が同様の課題を抱える開発者の参考になれば幸いです。SWRには他にも多くの便利な機能があるので、ぜひ公式ドキュメントもチェックしてみてください!
Discussion