🦔

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