はじめに
結論から言うと、TanStack Queryは2025年現在、Reactでのデータ取得において一択と言っていいレベルのライブラリです。
GitHubで47,700スター以上、毎月5,000万回以上のnpmダウンロード、72万以上のプロジェクトで利用されている。もはや「定番」どころか「必須」に近い存在になっていますね。
正直なところ、最初は「useEffectとuseStateで十分じゃないの?」と思っていたんですよ。データ取得なんて、fetchしてsetStateするだけだし。でも実際にプロダクションで運用し始めると、ローディング状態、エラーハンドリング、キャッシュ、再取得...と複雑化していく。30代になって思うのは、「自分で書ける」と「効率的に運用できる」は全く別の話だということ。
TanStack Queryとは
TanStack Query(旧React Query)は、Tanner Linsley氏が開発したサーバー状態管理ライブラリです。「Powerful asynchronous state management」がキャッチコピーで、データの取得、キャッシング、同期、更新を簡潔に扱えます。
現在の最新バージョンはv5.90.12で、MITライセンスのオープンソースとして公開されています。6年以上の歴史があり、1,026人以上のコントリビューターが開発に参加しています。
ちなみに、React以外にもVue、Solid、Svelte、Angular向けのアダプターも提供されていて、TanStackファミリーとして展開されています。
特徴・メリット
1. コード量が圧倒的に減る
これ、意外と数字で見ると衝撃的なんですけど、useEffectでのデータ取得コードと比較すると体感で60〜70%くらいコード量が減ります。
ローディング、エラー、成功状態の管理がすべて組み込まれているので、自前で書く必要がない。コスパ的に、この時短効果は計り知れないです。
2. キャッシュが自動で効く
同じクエリを複数のコンポーネントで使っても、自動的にキャッシュされます。重複したリクエストが飛ばないので、パフォーマンスも向上する。
個人的には、この「何も設定しなくても賢く動く」というのが一番のメリットだと思います。
3. バックグラウンド更新
ウィンドウにフォーカスが戻ったときや、ネットワーク再接続時に自動で再取得してくれる。ユーザーが常に最新データを見られるようになるので、UXがかなり上がります。
4. TypeScriptとの相性が抜群
型推論がしっかり効くので、TypeScriptで書くときのストレスがない。レスポンスの型定義をジェネリクスで渡すだけで、dataの型が推論されます。
5. DevToolsが便利
@tanstack/react-query-devtoolsを入れると、クエリの状態がビジュアルで確認できます。デバッグ時にめちゃくちゃ助かる。
インストール方法
npmまたはyarnでインストールします。
# npm
npm install @tanstack/react-query
# yarn
yarn add @tanstack/react-query
# pnpm
pnpm add @tanstack/react-query
DevToolsも一緒に入れておくと便利です。
npm install @tanstack/react-query-devtools
基本的な使い方
セットアップ
まず、アプリケーションのルートでQueryClientProviderを設定します。
// app/layout.tsx または App.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
const queryClient = new QueryClient()
export default function App({ children }: { children: React.ReactNode }) {
return (
<QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
)
}
データ取得(useQuery)
基本的なデータ取得はuseQueryフックを使います。
import { useQuery } from '@tanstack/react-query'
interface Todo {
id: number
title: string
completed: boolean
}
function TodoList() {
const { data, isPending, error } = useQuery<Todo[]>({
queryKey: ['todos'],
queryFn: () => fetch('/api/todos').then(res => res.json()),
})
if (isPending) return <div>読み込み中...</div>
if (error) return <div>エラーが発生しました</div>
return (
<ul>
{data?.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
)
}
これ、従来のuseEffect + useStateで書くと以下のようになっていたんですよね。
// 従来のやり方(比較用)
function TodoListOld() {
const [data, setData] = useState<Todo[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
fetch('/api/todos')
.then(res => res.json())
.then(data => {
setData(data)
setLoading(false)
})
.catch(err => {
setError(err)
setLoading(false)
})
}, [])
// 以下略...
}
明らかにコード量が違いますよね。
データ更新(useMutation)
データの作成・更新・削除にはuseMutationを使います。
import { useMutation, useQueryClient } from '@tanstack/react-query'
function AddTodo() {
const queryClient = useQueryClient()
const mutation = useMutation({
mutationFn: (newTodo: { title: string }) => {
return fetch('/api/todos', {
method: 'POST',
body: JSON.stringify(newTodo),
headers: { 'Content-Type': 'application/json' },
}).then(res => res.json())
},
onSuccess: () => {
// 成功したらtodosのキャッシュを無効化して再取得
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
const formData = new FormData(e.currentTarget)
mutation.mutate({ title: formData.get('title') as string })
}
return (
<form onSubmit={handleSubmit}>
<input name="title" placeholder="新しいTodo" />
<button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? '追加中...' : '追加'}
</button>
</form>
)
}
実践的なユースケース
パラメータ付きクエリ
ユーザーIDなどのパラメータを含むクエリの書き方です。
function UserProfile({ userId }: { userId: string }) {
const { data: user, isPending } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetch(`/api/users/${userId}`).then(res => res.json()),
enabled: !!userId, // userIdがあるときだけ実行
})
if (isPending) return <div>読み込み中...</div>
return <div>{user?.name}</div>
}
queryKeyにuserIdを含めることで、ユーザーごとに別のキャッシュが作られます。これ、地味に便利なんですよ。
無限スクロール
SNSのタイムラインのような無限スクロールも簡単に実装できます。
import { useInfiniteQuery } from '@tanstack/react-query'
function Timeline() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfiniteQuery({
queryKey: ['posts'],
queryFn: ({ pageParam }) =>
fetch(`/api/posts?cursor=${pageParam}`).then(res => res.json()),
initialPageParam: 0,
getNextPageParam: (lastPage) => lastPage.nextCursor,
})
return (
<>
{data?.pages.map((page, i) => (
<div key={i}>
{page.posts.map((post: { id: number; content: string }) => (
<article key={post.id}>{post.content}</article>
))}
</div>
))}
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
>
{isFetchingNextPage ? '読み込み中...' : 'もっと見る'}
</button>
</>
)
}
楽観的更新(Optimistic Updates)
ユーザー体験を向上させる楽観的更新も実装できます。
const likeMutation = useMutation({
mutationFn: (postId: string) =>
fetch(`/api/posts/${postId}/like`, { method: 'POST' }),
onMutate: async (postId) => {
// 進行中のクエリをキャンセル
await queryClient.cancelQueries({ queryKey: ['posts', postId] })
// 現在のデータを保存
const previousPost = queryClient.getQueryData(['posts', postId])
// 楽観的に更新
queryClient.setQueryData(['posts', postId], (old: any) => ({
...old,
likes: old.likes + 1,
}))
return { previousPost }
},
onError: (err, postId, context) => {
// エラー時はロールバック
queryClient.setQueryData(['posts', postId], context?.previousPost)
},
})
SWRとの比較
よく比較されるのがVercelのSWRですね。正直なところ、どちらも優秀で、小規模なプロジェクトならどちらを選んでも問題ない。
ただ、個人的にはTanStack Queryの方が機能が豊富で、特にuseMutationの使いやすさとDevToolsの完成度で一歩リードしている印象です。大規模なプロジェクトになるほど、TanStack Queryの恩恵を感じやすいと思います。
まとめ
TanStack Queryを導入すると、以下のメリットが得られます。
- データ取得コードが大幅に減る
- キャッシュ管理を自動化できる
- ローディング・エラー状態の管理が楽になる
- TypeScriptとの相性が良い
- DevToolsでデバッグが捗る
30代になって思うのは、「車輪の再発明」をしている時間はないということ。データ取得周りのボイラープレートを自分で書く時代は終わりました。TanStack Queryを使って、本質的なビジネスロジックに集中した方がQOLは確実に上がります。
useEffectでデータ取得を書いているなら、一度試してみることをおすすめします。一度使ったら、もう戻れなくなりますよ。