TanStack Query(React Query)完全ガイド – データフェッチングの最適化
API 連携の複雑さを解決する
React でのデータフェッチング(API 連携)は、一見シンプルに見えて、実は多くの課題を抱えています。「キャッシュをどう管理するか」「ローディング中の UI をどう表示するか」「エラー時の再試行をどうするか」「バックグラウンド更新をどう実装するか」——こうした実装パターンは、プロジェクトごとに何度も繰り返されます。
今回は、これらの課題を一括で解決するTanStack Query(旧 React Query)について、基本から応用実装まで、詳しくまとめておきたいと思います。
対象となる方
- React で API 連携しているが、データ管理が複雑になっている方
- キャッシング、バックグラウンド更新を実装したい方
- 大規模な API 連携アプリケーション開発を行う方
※ このドキュメントは TanStack Query v5 以上で書いていきます。
TanStack Query の基本
インストールと初期設定
npm install @tanstack/react-query
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient();
export function App() {
return (
<QueryClientProvider client={queryClient}>
{/* Your app */}
</QueryClientProvider>
);
}
useQuery の基本
useQuery は、サーバーからデータを取得し、キャッシュしてくれる hook です。
import { useQuery } from '@tanstack/react-query';
interface Post {
id: number;
title: string;
}
export function Posts() {
const { data, isLoading, error } = useQuery({
queryKey: ['posts'],
queryFn: async () => {
const response = await fetch('/api/posts');
return response.json() as Promise<Post[]>;
},
});
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<ul>
{data?.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
queryKey の重要性
queryKey はキャッシュ管理の基本です。配列の形式で複数のキーを組み合わせることで、細かいキャッシュ制御が可能になります。
// ✅ 良い例:パラメータを含める
const { data } = useQuery({
queryKey: ['posts', { page: 1, limit: 10 }],
queryFn: () => fetch('/api/posts?page=1&limit=10').then(r => r.json()),
});
// パラメータが異なれば別のキャッシュ
const { data: page2 } = useQuery({
queryKey: ['posts', { page: 2, limit: 10 }],
queryFn: () => fetch('/api/posts?page=2&limit=10').then(r => r.json()),
});
useMutation – データ変更
useMutation の基本
useMutation は、POST、PUT、DELETE などのデータ変更操作に使用します。
import { useMutation, useQueryClient } from '@tanstack/react-query';
export function CreatePostForm() {
const queryClient = useQueryClient();
const [title, setTitle] = useState('');
const mutation = useMutation({
mutationFn: async (newPost: { title: string }) => {
const response = await fetch('/api/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newPost),
});
return response.json();
},
onSuccess: () => {
// 成功時にキャッシュを無効化
queryClient.invalidateQueries({ queryKey: ['posts'] });
setTitle('');
},
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
mutation.mutate({ title });
};
return (
<form onSubmit={handleSubmit}>
<input value={title} onChange={(e) => setTitle(e.target.value)} />
<button disabled={mutation.isPending}>
{mutation.isPending ? '投稿中...' : '投稿'}
</button>
</form>
);
}
キャッシング戦略
staleTime と cacheTime
TanStack Query のキャッシング動作は、staleTime(有効期間)と cacheTime(メモリに保持する期間)で制御します。
const { data } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetch(`/api/users/${userId}`).then(r => r.json()),
staleTime: 1000 * 60 * 5, // 5分間は fresh(再取得しない)
gcTime: 1000 * 60 * 10, // 10分間メモリに保持
});
staleTime と gcTime の違い
| 項目 | 説明 | デフォルト |
| staleTime | データが fresh 状態の期間。この間は再取得しない | 0ms(常に stale) |
| gcTime | 使用されないクエリをメモリに保持する期間 | 5分 |
invalidateQueries でキャッシュを無効化
const queryClient = useQueryClient();
// 特定のキーを無効化
queryClient.invalidateQueries({ queryKey: ['posts'] });
// 部分一致で無効化
queryClient.invalidateQueries({
queryKey: ['posts'],
exact: false, // 'posts' を含むすべてのキーが対象
});
// useMutation の onSuccess で活用
const mutation = useMutation({
mutationFn: (newPost) => createPost(newPost),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['posts'] });
},
});
Background Refetching と Polling
バックグラウンド更新
データが stale 状態になると、バックグラウンドで自動的に再取得されます。
const { data, isFetching } = useQuery({
queryKey: ['posts'],
queryFn: () => fetch('/api/posts').then(r => r.json()),
staleTime: 1000 * 60, // 1分後に stale
refetchOnWindowFocus: true, // ウィンドウがフォーカスされたら再取得
refetchOnMount: true, // コンポーネントがマウントされたら再取得
});
return (
<div>
{isFetching && <p>Updating...</p>}
{/* データ表示 */}
</div>
);
Polling:定期的な更新
const { data } = useQuery({
queryKey: ['stock-price'],
queryFn: () => fetch('/api/stock-price').then(r => r.json()),
refetchInterval: 1000 * 5, // 5秒ごとに再取得
refetchIntervalInBackground: true, // タブがバックグラウンドでも再取得
});
エラーハンドリングと再試行
retry の設定
ネットワークエラーの場合、自動的に再試行できます。
const { data, error } = useQuery({
queryKey: ['posts'],
queryFn: async () => {
const response = await fetch('/api/posts');
if (!response.ok) throw new Error('Failed to fetch');
return response.json();
},
retry: 3, // 失敗時に最大3回まで再試行
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
// 指数バックオフ:1秒、2秒、4秒...
});
カスタムエラーハンドリング
const { data, error, isError } = useQuery({
queryKey: ['user'],
queryFn: () => fetch('/api/user').then(r => r.json()),
retry: (failureCount, error) => {
// 特定のステータスコードは再試行しない
if (error instanceof Response && error.status === 404) {
return false;
}
return failureCount < 3;
},
});
if (isError) {
return <div>Error loading user: {error?.message}</div>;
}
実装例:ページング
ページング実装
export function PaginatedPosts() {
const [page, setPage] = useState(1);
const { data, isLoading, isPreviousData } = useQuery({
queryKey: ['posts', page],
queryFn: () => fetch(`/api/posts?page=${page}`).then(r => r.json()),
placeholderData: (previousData) => previousData, // ページ遷移時に前のデータを表示
});
return (
<>
{data?.posts.map((post) => (
<div key={post.id} style={{ opacity: isPreviousData ? 0.5 : 1 }}>
{post.title}
</div>
))}
<button onClick={() => setPage((p) => Math.max(p - 1, 1))}>
Previous
</button>
<button onClick={() => setPage((p) => p + 1)}>Next</button>
</>
);
}
実装例:無限スクロール
useInfiniteQuery でのスクロール実装
import { useInfiniteQuery } from '@tanstack/react-query';
export function InfinitePosts() {
const {
data,
hasNextPage,
fetchNextPage,
isFetchingNextPage,
} = useInfiniteQuery({
queryKey: ['posts'],
queryFn: async ({ pageParam = 1 }) => {
const response = await fetch(`/api/posts?page=${pageParam}`);
return response.json();
},
getNextPageParam: (lastPage, pages) => {
return lastPage.hasMore ? pages.length + 1 : undefined;
},
});
const sentinelRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && hasNextPage && !isFetchingNextPage) {
fetchNextPage();
}
});
if (sentinelRef.current) {
observer.observe(sentinelRef.current);
}
return () => observer.disconnect();
}, [hasNextPage, isFetchingNextPage, fetchNextPage]);
return (
<>
{data?.pages.map((page) =>
page.posts.map((post) => <div key={post.id}>{post.title}</div>)
)}
<div ref={sentinelRef} style={{ height: '100px' }}>
{isFetchingNextPage ? 'Loading more...' : ''}
</div>
</>
);
}
フォーム送信との連携
Optimistic Update
API リクエスト前にローカルで状態を更新し、レスポンス待機中も UI が反応的に見えるようにします。
export function UpdateUserForm({ userId }: { userId: number }) {
const queryClient = useQueryClient();
const [name, setName] = useState('');
const mutation = useMutation({
mutationFn: async (newName: string) => {
const response = await fetch(`/api/users/${userId}`, {
method: 'PUT',
body: JSON.stringify({ name: newName }),
});
return response.json();
},
onMutate: async (newName) => {
// キャッシュを一時的に更新(Optimistic Update)
await queryClient.cancelQueries({ queryKey: ['user', userId] });
const previousData = queryClient.getQueryData(['user', userId]);
queryClient.setQueryData(['user', userId], (old: any) => ({
...old,
name: newName,
}));
return { previousData };
},
onError: (error, newName, context) => {
// エラー時はロールバック
queryClient.setQueryData(
['user', userId],
context?.previousData
);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['user', userId] });
},
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
mutation.mutate(name);
};
return (
<form onSubmit={handleSubmit}>
<input value={name} onChange={(e) => setName(e.target.value)} />
<button disabled={mutation.isPending}>Update</button>
</form>
);
}
DevTools でデバッグ
npm install @tanstack/react-query-devtools
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
export function App() {
return (
<QueryClientProvider client={queryClient}>
{/* Your app */}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
まとめ
以上で TanStack Query 完全ガイドを終えたいと思います。
TanStack Query は、React でのデータフェッチングを大幅に簡潔にし、キャッシング、バックグラウンド更新、エラーハンドリングなどの複雑な実装パターンを一気に解決してくれます。useQuery、useMutation、useInfiniteQuery などの hooks を理解することで、大規模な API 連携アプリケーションでも、保守性高いコード構造が実現できます。
ぜひ TanStack Query をプロジェクトに導入して、データフェッチング周りの実装コストを削減し、機能開発に集中しましょう。
