🦔
React Queryで実現する賢いデータ更新戦略 - refetchIntervalとinvalidateの使い分け
はじめに
React Queryを使ったデータ取得において、「いつ、どのようにデータを更新するか」は重要な設計判断です。今回は、実際のプロダクト開発で遭遇した「非同期処理の進捗監視」という課題を通じて、refetchIntervalとinvalidateを組み合わせた効果的なデータ更新パターンを紹介します。
課題:非同期処理の状態をリアルタイムに反映したい
ECサイトの管理画面で、複数の商品データを外部APIから取得する機能を実装していました。要件は以下の通りです:
- 処理には数秒〜数分かかる
- 処理開始直後にUIを更新したい
- 処理中は進捗をリアルタイムで表示したい
- 処理完了後は自動更新を停止したい
2つのアプローチの特徴
1. refetchInterval:定期的な自動更新
const { data, isLoading } = useQuery({
queryKey: ['items', listId],
queryFn: fetchItemList,
refetchInterval: 5000, // 5秒ごとに自動更新
})
メリット:
- 設定が簡単
- 自動的に最新データを取得
デメリット:
- 常に通信が発生(処理していない時も)
- ユーザーアクション直後の更新には不向き
2. invalidateQueries:任意のタイミングでの更新
const queryClient = useQueryClient()
const updateMutation = useMutation({
mutationFn: updateItem,
onSuccess: () => {
// キャッシュを無効化して再取得
queryClient.invalidateQueries({ queryKey: ['items', listId] })
}
})
メリット:
- 必要な時だけ更新
- 即座に反映
デメリット:
- 都度手動で呼び出す必要がある
- 継続的な監視には不向き
解決策:動的なrefetchIntervalとinvalidateの組み合わせ
これらを組み合わせることで、両方のメリットを活かせます:
function ItemManager() {
// 処理中のアイテムIDを管理
const [processingItems, setProcessingItems] = useState<Set<string>>(new Set())
// データ取得(処理中のみ自動更新)
const { data: items } = useQuery({
queryKey: ['items', listId],
queryFn: fetchItemList,
// 処理中のアイテムがある時だけ自動更新ON
refetchInterval: processingItems.size > 0 ? 5000 : false,
})
const queryClient = useQueryClient()
// アイテム処理の実行
const processMutation = useMutation({
mutationFn: processItem,
onSuccess: () => {
// 処理開始直後に画面を更新
queryClient.invalidateQueries({ queryKey: ['items', listId] })
}
})
const handleProcess = async (itemId: string) => {
try {
// 処理中リストに追加
setProcessingItems(prev => new Set(prev).add(itemId))
await processMutation.mutateAsync({ itemId })
} finally {
// 処理完了後にリストから削除
setProcessingItems(prev => {
const newSet = new Set(prev)
newSet.delete(itemId)
return newSet
})
}
}
return (
<div>
{items?.map(item => (
<ItemCard
key={item.id}
item={item}
isProcessing={processingItems.has(item.id)}
onProcess={() => handleProcess(item.id)}
/>
))}
</div>
)
}
動作フロー
なぜこのパターンが効果的なのか
1. リソースの効率的な利用
// ❌ 非効率:常に5秒ごとに更新
refetchInterval: 5000
// ✅ 効率的:必要な時だけ更新
refetchInterval: processingItems.size > 0 ? 5000 : false
2. 優れたユーザー体験
- アクション直後の即座のフィードバック(invalidate)
- 処理中の自動的な進捗更新(refetchInterval)
- 完了後の無駄な通信削減
3. 実装の見通しの良さ
// 処理状態が一目でわかる
const isAnyProcessing = processingItems.size > 0
const isThisItemProcessing = processingItems.has(itemId)
応用例:ファイルアップロード管理
同じパターンは様々な場面で活用できます:
function FileUploadManager() {
const [uploadingFiles, setUploadingFiles] = useState<Set<string>>(new Set())
const { data: files } = useQuery({
queryKey: ['files'],
queryFn: fetchFiles,
refetchInterval: uploadingFiles.size > 0 ? 3000 : false,
})
const queryClient = useQueryClient()
const uploadMutation = useMutation({
mutationFn: uploadFile,
onMutate: ({ fileId }) => {
setUploadingFiles(prev => new Set(prev).add(fileId))
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['files'] })
},
onSettled: (_, __, { fileId }) => {
setUploadingFiles(prev => {
const newSet = new Set(prev)
newSet.delete(fileId)
return newSet
})
}
})
// アップロード進捗の表示
const uploadProgress = files?.filter(
file => uploadingFiles.has(file.id)
).length || 0
return (
<div>
{uploadProgress > 0 && (
<div>アップロード中: {uploadProgress}件</div>
)}
{/* ファイル一覧 */}
</div>
)
}
パフォーマンス最適化のTips
1. 更新間隔の動的調整
// 処理数に応じて間隔を調整
const getRefetchInterval = (count: number) => {
if (count === 0) return false
if (count < 5) return 5000 // 少ない時は5秒
if (count < 10) return 10000 // 多い時は10秒
return 15000 // とても多い時は15秒
}
refetchInterval: getRefetchInterval(processingItems.size)
2. 選択的な無効化
// 特定の条件下でのみ無効化
onSuccess: (data) => {
if (data.affectsOtherItems) {
queryClient.invalidateQueries({ queryKey: ['items'] })
}
}
まとめ
React QueryのrefetchIntervalとinvalidateQueriesを組み合わせることで、効率的でユーザー体験の良いデータ更新を実現できます。
ポイント:
- 即座の更新が必要 → invalidate
- 継続的な監視が必要 → refetchInterval
- 両方必要 → 動的なrefetchInterval + invalidate
このパターンは、非同期処理の進捗管理だけでなく、リアルタイムダッシュボード、通知システム、コラボレーションツールなど、様々な場面で活用できる汎用的なテクニックです。
Discussion