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になる
gcTime は staleTime より長く設定するのが基本です。
よく使うオプション
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入門」を参照してください。