TanStack Query 入門:useQueryでデータフェッチを簡単に

スポンサーリンク

TanStack Query 入門:useQueryでデータフェッチを簡単に

TanStack Queryとは

TanStack Query(旧React Query)はReactでのデータフェッチ・キャッシュ・同期を管理するライブラリです。useEffect + useState で自前実装していたローディング・エラー・キャッシュの処理を大幅に簡略化できます。


useEffectでのデータフェッチの問題

// 自前実装は毎回これを書くことになる
function UserList() {
  const [users, setUsers] = useState([])
  const [loading, setLoading] = useState(false)
  const [error, setError] = useState(null)

  useEffect(() => {
    setLoading(true)
    fetch('/api/users')
      .then((r) => r.json())
      .then((data) => { setUsers(data); setLoading(false) })
      .catch((e) => { setError(e); setLoading(false) })
  }, [])

  if (loading) return <p>読み込み中...</p>
  if (error) return <p>エラー</p>
  return <ul>{users.map(...)}</ul>
}

TanStack Queryを使うとこれが数行になります。


インストール

npm install @tanstack/react-query

セットアップ

// src/main.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'

const queryClient = new QueryClient()

createRoot(document.getElementById('root')!).render(
  <QueryClientProvider client={queryClient}>
    <App />
  </QueryClientProvider>
)

useQueryで取得する

import { useQuery } from '@tanstack/react-query'

type User = { id: number; name: string }

async function fetchUsers(): Promise<User[]> {
  const res = await fetch('/api/users')
  if (!res.ok) throw new Error('取得失敗')
  return res.json()
}

function UserList() {
  const { data, isLoading, isError } = useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers,
  })

  if (isLoading) return <p>読み込み中...</p>
  if (isError) return <p>エラーが発生しました</p>

  return (
    <ul>
      {data?.map((user) => <li key={user.id}>{user.name}</li>)}
    </ul>
  )
}

queryKey はキャッシュのキーです。同じキーで複数コンポーネントからfetchしてもリクエストは1回に集約されます。


パラメータ付きのクエリ

function UserDetail({ id }: { id: number }) {
  const { data } = useQuery({
    queryKey: ['users', id], // idが変わると再取得
    queryFn: () => fetch(`/api/users/${id}`).then((r) => r.json()),
  })

  return <p>{data?.name}</p>
}

useMutationで更新する

import { useMutation, useQueryClient } from '@tanstack/react-query'

function AddUser() {
  const queryClient = useQueryClient()

  const mutation = useMutation({
    mutationFn: (name: string) =>
      fetch('/api/users', {
        method: 'POST',
        body: JSON.stringify({ name }),
        headers: { 'Content-Type': 'application/json' },
      }).then((r) => r.json()),
    onSuccess: () => {
      // usersキャッシュを無効化して再取得
      queryClient.invalidateQueries({ queryKey: ['users'] })
    },
  })

  return (
    <button onClick={() => mutation.mutate('新しいユーザー')}>
      {mutation.isPending ? '送信中...' : '追加'}
    </button>
  )
}

キャッシュを直接更新する(setQueryData)

invalidateQueries はキャッシュを無効化してサーバーから再取得しますが、setQueryData を使うとネットワークリクエストなしにキャッシュを直接書き換えられます。

const queryClient = useQueryClient()

const mutation = useMutation({
  mutationFn: (newUser: { name: string }) =>
    fetch('/api/users', {
      method: 'POST',
      body: JSON.stringify(newUser),
      headers: { 'Content-Type': 'application/json' },
    }).then((r) => r.json()),

  onSuccess: (createdUser: User) => {
    // 再取得せず、キャッシュに直接追加
    queryClient.setQueryData<User[]>(['users'], (prev = []) => [
      ...prev,
      createdUser,
    ])
  },
})

invalidateQueries との使い分け:

invalidateQueries setQueryData
ネットワーク 再取得する しない
向いている場面 サーバー側の変更が複雑 レスポンスをそのまま使える
データの正確さ サーバーと同期 手動管理

オプティミスティック更新

サーバーのレスポンスを待たずに UI を先に更新し、失敗したらロールバックするパターンです。

const mutation = useMutation({
  mutationFn: (name: string) =>
    fetch('/api/users', { method: 'POST', body: JSON.stringify({ name }) }).then(r => r.json()),

  onMutate: async (name) => {
    // 進行中のリフェッチをキャンセル
    await queryClient.cancelQueries({ queryKey: ['users'] })

    // 現在のキャッシュを保存(ロールバック用)
    const previous = queryClient.getQueryData<User[]>(['users'])

    // 楽観的にキャッシュを更新
    queryClient.setQueryData<User[]>(['users'], (prev = []) => [
      ...prev,
      { id: Date.now(), name }, // 仮のID
    ])

    return { previous }
  },

  onError: (_err, _name, context) => {
    // 失敗したら元に戻す
    queryClient.setQueryData(['users'], context?.previous)
  },

  onSettled: () => {
    // 成功・失敗に関わらず最終的に再取得
    queryClient.invalidateQueries({ queryKey: ['users'] })
  },
})

staleTime と gcTime の違い

混乱しやすい2つのオプションです。

useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,
  staleTime: 1000 * 60 * 5,  // 5分
  gcTime: 1000 * 60 * 10,    // 10分(旧称: cacheTime)
})
オプション 意味 経過後の動作
staleTime データを「新鮮」とみなす時間 期限切れ後、次回アクセス時に再取得
gcTime キャッシュをメモリに保持する時間 期限切れ後、キャッシュを完全に削除
  • staleTime を過ぎた → バックグラウンドで再取得するが、古いデータを即座に表示する
  • gcTime を過ぎた → キャッシュが消え、次回は必ず isLoading: true になる

gcTimestaleTime より長く設定するのが基本です。


よく使うオプション

useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,
  staleTime: 1000 * 60 * 5,  // 5分間はキャッシュを新鮮とみなす
  retry: 2,                   // 失敗時のリトライ回数
  enabled: !!userId,          // falseのときfetchしない
})
オプション 説明
staleTime キャッシュを再取得しない時間(ms)
retry エラー時のリトライ回数(デフォルト3)
enabled falseのときクエリを実行しない
refetchOnWindowFocus ウィンドウフォーカス時に再取得するか

まとめ

用途 API
データ取得(GET) useQuery
データ変更(POST/PUT/DELETE) useMutation
キャッシュ無効化・再取得 queryClient.invalidateQueries
キャッシュを直接書き換え queryClient.setQueryData
キャッシュを読み取り queryClient.getQueryData
データを「新鮮」とみなす時間 staleTime
キャッシュ保持時間 gcTime
  • useEffect + useState の自前実装をまとめて置き換えられる
  • キャッシュ・ローディング・エラー処理が自動化される
  • queryKey でキャッシュが管理され、関連データの無効化が簡単

OpenAPIとの組み合わせは「orvalでOpenAPIからReact Queryフックを自動生成する」を参照してください。

useEffect の詳細は「ReactのuseEffect入門」を参照してください。