TanStack Query(React Query)完全ガイド!データフェッチングとキャッシュ戦略の実践的な使い方

TanStack Queryとは何か
TanStack Query(旧React Query)は、Reactアプリケーションでサーバーサイドのデータを効率的に管理するためのライブラリです。従来のfetchやaxiosを使った単純なデータ取得と比べて、以下のような強力な機能を提供しています。
主な特徴:
- 自動キャッシュ管理: 一度取得したデータを自動でキャッシュし、同じデータへの重複リクエストを防止
- バックグラウンド更新: ユーザーが気づかないうちにデータを最新状態に保つ
- エラーハンドリング: 統一されたエラー処理とリトライ機能
- ローディング状態: データ取得中の状態を簡単に管理
従来の方法との比較:
// 従来のuseEffectを使った方法
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch('/api/users')
.then(res => res.json())
.then(setData)
.catch(setError)
.finally(() => setLoading(false));
}, []);
// TanStack Queryを使った方法
const { data, isLoading, error } = useQuery({
queryKey: ['users'],
queryFn: () => fetch('/api/users').then(res => res.json())
});
このように、TanStack Queryを使うことで複雑な状態管理を大幅に簡素化できます。
TanStack Queryのセットアップと基本設定
TanStack Queryを始めるには、まずパッケージをインストールし、アプリケーション全体でQueryClientを設定する必要があります。
インストール:
# npm を使用する場合
npm install @tanstack/react-query
# yarn を使用する場合
yarn add @tanstack/react-query
基本的なセットアップ:
// App.js または _app.js
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
// QueryClientインスタンスを作成
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5分間はデータを新鮮として扱う
cacheTime: 10 * 60 * 1000, // 10分間キャッシュを保持
retry: 1, // 失敗時のリトライ回数
refetchOnWindowFocus: false, // ウィンドウフォーカス時の再取得を無効化
},
},
});
function App() {
return (
<QueryClientProvider client={queryClient}>
<YourAppComponents />
{/* 開発時のデバッグツール */}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
export default App;
Next.jsでの設定例:
// _app.js
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useState } from 'react';
export default function MyApp({ Component, pageProps }) {
const [queryClient] = useState(() => new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 1分
},
},
}));
return (
<QueryClientProvider client={queryClient}>
<Component {...pageProps} />
</QueryClientProvider>
);
}
このセットアップにより、アプリケーション全体でTanStack Queryの機能を使用できるようになります。
useQueryフックでデータを取得する方法
useQueryは、サーバーからデータを取得するためのメインとなるフックです。基本的な使い方から実践的なパターンまで、順を追って解説します。
基本的な使い方:
import { useQuery } from '@tanstack/react-query';
function UsersList() {
const { data, isLoading, error } = useQuery({
queryKey: ['users'],
queryFn: async () => {
const response = await fetch('/api/users');
if (!response.ok) {
throw new Error('ユーザー取得に失敗しました');
}
return response.json();
}
});
if (isLoading) return <div>読み込み中...</div>;
if (error) return <div>エラー: {error.message}</div>;
return (
<ul>
{data.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
動的なクエリキーの使用:
function UserProfile({ userId }) {
const { data: user, isLoading } = useQuery({
queryKey: ['user', userId], // userIdが変わるとクエリが再実行される
queryFn: () => fetch(`/api/users/${userId}`).then(res => res.json()),
enabled: !!userId, // userIdが存在する場合のみクエリを実行
});
if (isLoading) return <div>ユーザー情報を読み込み中...</div>;
return (
<div>
<h2>{user?.name}</h2>
<p>Email: {user?.email}</p>
</div>
);
}
条件付きクエリとカスタム設定:
function PostsList({ category, page = 1 }) {
const {
data,
isLoading,
isFetching,
isError
} = useQuery({
queryKey: ['posts', category, page],
queryFn: async () => {
const response = await fetch(`/api/posts?category=${category}&page=${page}`);
return response.json();
},
enabled: !!category, // categoryが選択されている場合のみ実行
staleTime: 2 * 60 * 1000, // 2分間は新鮮なデータとして扱う
cacheTime: 5 * 60 * 1000, // 5分間キャッシュを保持
retry: 3, // 失敗時に3回リトライ
keepPreviousData: true, // ページング時に前のデータを保持
});
return (
<div>
{isFetching && <div>更新中...</div>}
{isError && <div>投稿の取得に失敗しました</div>}
{data?.posts?.map(post => (
<article key={post.id}>
<h3>{post.title}</h3>
<p>{post.excerpt}</p>
</article>
))}
</div>
);
}
axiosを使った実装例:
import axios from 'axios';
const api = axios.create({
baseURL: 'https://api.example.com',
timeout: 5000,
});
function ProductList() {
const { data: products, isLoading, error } = useQuery({
queryKey: ['products'],
queryFn: async () => {
const { data } = await api.get('/products');
return data;
},
onError: (error) => {
console.error('商品取得エラー:', error);
},
onSuccess: (data) => {
console.log('商品取得成功:', data);
}
});
// レンダリング処理...
}
このように、useQueryは様々な設定オプションを組み合わせることで、柔軟なデータ取得処理を実現できます。
useMutationでデータを更新する実装
useMutationは、データの作成、更新、削除などの副作用を伴う操作を行うためのフックです。サーバーのデータを変更した後に、適切にキャッシュを更新する方法を学びましょう。
基本的なuseMutationの使い方:
import { useMutation, useQueryClient } from '@tanstack/react-query';
function CreateUserForm() {
const queryClient = useQueryClient();
const createUserMutation = useMutation({
mutationFn: async (userData) => {
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData),
});
if (!response.ok) {
throw new Error('ユーザー作成に失敗しました');
}
return response.json();
},
onSuccess: () => {
// 成功時にユーザーリストのキャッシュを無効化して再取得
queryClient.invalidateQueries({ queryKey: ['users'] });
},
onError: (error) => {
console.error('ユーザー作成エラー:', error);
}
});
const handleSubmit = (formData) => {
createUserMutation.mutate({
name: formData.name,
email: formData.email,
});
};
return (
<form onSubmit={handleSubmit}>
{createUserMutation.isLoading && <p>作成中...</p>}
{createUserMutation.isError && (
<p>エラー: {createUserMutation.error.message}</p>
)}
{createUserMutation.isSuccess && <p>ユーザーが作成されました!</p>}
{/* フォームフィールド */}
</form>
);
}
Optimistic Updates(楽観的更新)の実装:
function TodoList() {
const queryClient = useQueryClient();
const toggleTodoMutation = useMutation({
mutationFn: async ({ id, completed }) => {
const response = await fetch(`/api/todos/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ completed }),
});
return response.json();
},
// 楽観的更新:APIレスポンスを待たずに即座にUIを更新
onMutate: async ({ id, completed }) => {
// 進行中のクエリをキャンセル
await queryClient.cancelQueries({ queryKey: ['todos'] });
// 現在のデータを取得
const previousTodos = queryClient.getQueryData(['todos']);
// 楽観的にデータを更新
queryClient.setQueryData(['todos'], old =>
old.map(todo =>
todo.id === id ? { ...todo, completed } : todo
)
);
// ロールバック用に以前のデータを返す
return { previousTodos };
},
onError: (err, variables, context) => {
// エラー時にロールバック
if (context?.previousTodos) {
queryClient.setQueryData(['todos'], context.previousTodos);
}
},
onSettled: () => {
// 成功・失敗に関わらず最終的にデータを再取得
queryClient.invalidateQueries({ queryKey: ['todos'] });
}
});
// 使用例
const handleToggle = (todo) => {
toggleTodoMutation.mutate({
id: todo.id,
completed: !todo.completed
});
};
}
データ削除とキャッシュ更新:
function UserItem({ user }) {
const queryClient = useQueryClient();
const deleteUserMutation = useMutation({
mutationFn: async (userId) => {
await fetch(`/api/users/${userId}`, { method: 'DELETE' });
},
onSuccess: (_, deletedUserId) => {
// キャッシュから削除されたユーザーを除去
queryClient.setQueryData(['users'], (oldUsers) =>
oldUsers.filter(user => user.id !== deletedUserId)
);
}
});
return (
<div>
<span>{user.name}</span>
<button
onClick={() => deleteUserMutation.mutate(user.id)}
disabled={deleteUserMutation.isLoading}
>
{deleteUserMutation.isLoading ? '削除中...' : '削除'}
</button>
</div>
);
}
複数のクエリを同時に更新:
function UpdatePostForm({ postId }) {
const queryClient = useQueryClient();
const updatePostMutation = useMutation({
mutationFn: async (postData) => {
const response = await fetch(`/api/posts/${postId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(postData),
});
return response.json();
},
onSuccess: (updatedPost) => {
// 投稿詳細のキャッシュを更新
queryClient.setQueryData(['post', postId], updatedPost);
// 投稿一覧のキャッシュも更新
queryClient.setQueryData(['posts'], (oldPosts) =>
oldPosts.map(post =>
post.id === postId ? updatedPost : post
)
);
// 関連するクエリを無効化
queryClient.invalidateQueries({ queryKey: ['posts', 'recent'] });
}
});
}
useMutationを適切に使用することで、ユーザー体験を向上させる高速で信頼性の高いデータ更新処理を実現できます。
エラーハンドリングとローディング状態の管理
TanStack Queryでは、エラーハンドリングとローディング状態を効率的に管理できます。実用的なパターンとベストプラクティスを紹介します。
基本的なエラーハンドリング:
function UserProfile({ userId }) {
const {
data,
isLoading,
error,
isError,
refetch
} = useQuery({
queryKey: ['user', userId],
queryFn: async () => {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
// HTTPステータスに応じた詳細なエラー情報
const errorData = await response.json();
throw new Error(errorData.message || `エラー: ${response.status}`);
}
return response.json();
},
retry: (failureCount, error) => {
// 404エラーはリトライしない
if (error.message.includes('404')) return false;
// 3回までリトライ
return failureCount < 3;
},
retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000)
});
if (isLoading) {
return (
<div className="loading-container">
<div className="spinner"></div>
<p>ユーザー情報を読み込み中...</p>
</div>
);
}
if (isError) {
return (
<div className="error-container">
<h3>エラーが発生しました</h3>
<p>{error.message}</p>
<button onClick={() => refetch()}>再試行</button>
</div>
);
}
return <div>{data?.name}</div>;
}
グローバルエラーハンドリング:
// QueryClientでグローバルエラーハンドラーを設定
const queryClient = new QueryClient({
defaultOptions: {
queries: {
onError: (error) => {
// トースト通知を表示
toast.error(`データ取得エラー: ${error.message}`);
// エラーログを送信
console.error('Query Error:', error);
},
retry: (failureCount, error) => {
// ネットワークエラーの場合のみリトライ
if (error.name === 'NetworkError') return failureCount < 2;
return false;
}
},
mutations: {
onError: (error, variables, context) => {
toast.error(`操作に失敗しました: ${error.message}`);
console.error('Mutation Error:', error, variables);
}
}
}
});
高度なローディング状態管理:
function DataDashboard() {
const usersQuery = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
});
const postsQuery = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
});
const statsQuery = useQuery({
queryKey: ['stats'],
queryFn: fetchStats,
enabled: usersQuery.isSuccess && postsQuery.isSuccess
});
// 複数クエリの状態を統合
const isLoading = usersQuery.isLoading || postsQuery.isLoading;
const hasError = usersQuery.isError || postsQuery.isError;
const isRefetching = usersQuery.isFetching || postsQuery.isFetching;
if (isLoading) {
return (
<div className="dashboard-loading">
<div className="loading-items">
<div className={`loading-item ${usersQuery.isLoading ? 'active' : 'complete'}`}>
ユーザーデータ読み込み中...
</div>
<div className={`loading-item ${postsQuery.isLoading ? 'active' : 'complete'}`}>
投稿データ読み込み中...
</div>
</div>
</div>
);
}
return (
<div className="dashboard">
{isRefetching && (
<div className="refresh-indicator">データを更新中...</div>
)}
{hasError && (
<div className="error-banner">
一部のデータの取得に失敗しました
</div>
)}
{/* ダッシュボードコンテンツ */}
</div>
);
}
カスタムフックでローディング状態を抽象化:
function useApiState(queries) {
const states = queries.map(query => ({
isLoading: query.isLoading,
isError: query.isError,
isFetching: query.isFetching,
error: query.error,
}));
return {
isLoading: states.some(state => state.isLoading),
isError: states.some(state => state.isError),
isFetching: states.some(state => state.isFetching),
errors: states.filter(state => state.error).map(state => state.error),
loadingCount: states.filter(state => state.isLoading).length,
errorCount: states.filter(state => state.isError).length,
};
}
// 使用例
function MyComponent() {
const usersQuery = useQuery({ queryKey: ['users'], queryFn: fetchUsers });
const postsQuery = useQuery({ queryKey: ['posts'], queryFn: fetchPosts });
const apiState = useApiState([usersQuery, postsQuery]);
if (apiState.isLoading) {
return <div>読み込み中... ({apiState.loadingCount}/2)</div>;
}
if (apiState.isError) {
return (
<div>
エラーが発生しました ({apiState.errorCount}件)
{apiState.errors.map((error, i) => (
<p key={i}>{error.message}</p>
))}
</div>
);
}
}
Error Boundaryとの連携:
import { ErrorBoundary } from 'react-error-boundary';
function ErrorFallback({ error, resetErrorBoundary }) {
return (
<div className="error-fallback">
<h2>予期しないエラーが発生しました</h2>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>再試行</button>
</div>
);
}
function App() {
const queryClient = useQueryClient();
return (
<ErrorBoundary
FallbackComponent={ErrorFallback}
onReset={() => {
// すべてのクエリを再取得
queryClient.invalidateQueries();
}}
>
<YourAppComponents />
</ErrorBoundary>
);
}
これらのパターンを組み合わせることで、ユーザーフレンドリーで堅牢なエラーハンドリングシステムを構築できます。
キャッシュ戦略とパフォーマンス最適化の実践
TanStack Queryの真価は、効率的なキャッシュ戦略にあります。適切なキャッシュ設定により、アプリケーションのパフォーマンスを大幅に向上させることができます。
staleTimeとcacheTimeの理解:
const queryClient = new QueryClient({
defaultOptions: {
queries: {
// staleTime: データが「古い」とみなされるまでの時間
staleTime: 5 * 60 * 1000, // 5分間は新鮮なデータとして扱う
// cacheTime: キャッシュされたデータが保持される時間
cacheTime: 10 * 60 * 1000, // 10分間メモリに保持
// refetchOnWindowFocus: ウィンドウフォーカス時の再取得
refetchOnWindowFocus: false,
// refetchOnReconnect: ネットワーク再接続時の再取得
refetchOnReconnect: true,
},
},
});
データタイプ別キャッシュ戦略:
// ユーザープロフィール(変更頻度低)
const { data: userProfile } = useQuery({
queryKey: ['user', userId],
queryFn: fetchUserProfile,
staleTime: 30 * 60 * 1000, // 30分
cacheTime: 60 * 60 * 1000, // 1時間
});
// ニュースフィード(リアルタイム性重視)
const { data: newsFeed } = useQuery({
queryKey: ['news'],
queryFn: fetchNews,
staleTime: 30 * 1000, // 30秒
cacheTime: 5 * 60 * 1000, // 5分
refetchInterval: 60 * 1000, // 1分間隔で自動更新
});
// 商品カタログ(比較的安定)
const { data: products } = useQuery({
queryKey: ['products', category],
queryFn: () => fetchProducts(category),
staleTime: 10 * 60 * 1000, // 10分
cacheTime: 30 * 60 * 1000, // 30分
});
プリフェッチによる先読み戦略:
function ProductCatalog() {
const queryClient = useQueryClient();
const { data: currentProducts } = useQuery({
queryKey: ['products', currentPage],
queryFn: () => fetchProducts(currentPage),
});
// ページネーション用の先読み
useEffect(() => {
if (currentPage < totalPages) {
queryClient.prefetchQuery({
queryKey: ['products', currentPage + 1],
queryFn: () => fetchProducts(currentPage + 1),
staleTime: 10 * 60 * 1000,
});
}
}, [currentPage, queryClient, totalPages]);
// マウスホバーで商品詳細を先読み
const handleProductHover = (productId) => {
queryClient.prefetchQuery({
queryKey: ['product', productId],
queryFn: () => fetchProductDetail(productId),
});
};
return (
<div>
{currentProducts?.map(product => (
<div
key={product.id}
onMouseEnter={() => handleProductHover(product.id)}
>
{product.name}
</div>
))}
</div>
);
}
インテリジェントな依存関係管理:
function UserDashboard({ userId }) {
// ユーザー基本情報
const userQuery = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
});
// ユーザーの投稿(ユーザー情報取得後に実行)
const postsQuery = useQuery({
queryKey: ['posts', userId],
queryFn: () => fetchUserPosts(userId),
enabled: !!userQuery.data?.id,
staleTime: 2 * 60 * 1000,
});
// ユーザーの設定(必要時のみ)
const settingsQuery = useQuery({
queryKey: ['settings', userId],
queryFn: () => fetchUserSettings(userId),
enabled: false, // 手動で実行
});
const loadSettings = () => {
settingsQuery.refetch();
};
return (
<div>
{userQuery.data && (
<>
<UserProfile user={userQuery.data} />
<button onClick={loadSettings}>設定を読み込む</button>
</>
)}
</div>
);
}
メモリ使用量の最適化:
// 無限スクロール用の最適化
function InfinitePostsList() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfiniteQuery({
queryKey: ['posts'],
queryFn: ({ pageParam = 0 }) => fetchPosts(pageParam),
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
// 古いページのデータを自動的に削除
cacheTime: 5 * 60 * 1000, // 5分
// 最大ページ数を制限してメモリ使用量を抑制
maxPages: 10,
});
return (
<div>
{data?.pages.map((page, i) => (
<div key={i}>
{page.posts.map(post => (
<div key={post.id}>{post.title}</div>
))}
</div>
))}
{hasNextPage && (
<button onClick={fetchNextPage} disabled={isFetchingNextPage}>
{isFetchingNextPage ? '読み込み中...' : 'さらに読み込む'}
</button>
)}
</div>
);
}
キャッシュの手動管理:
function CacheManager() {
const queryClient = useQueryClient();
const clearUserCache = () => {
// 特定のユーザーのキャッシュをクリア
queryClient.removeQueries({ queryKey: ['user'] });
};
const refreshAllData = () => {
// すべてのクエリを無効化して再取得
queryClient.invalidateQueries();
};
const preloadCriticalData = () => {
// 重要なデータを事前読み込み
queryClient.prefetchQuery({
queryKey: ['user', 'current'],
queryFn: fetchCurrentUser,
});
queryClient.prefetchQuery({
queryKey: ['notifications'],
queryFn: fetchNotifications,
});
};
return (
<div>
<button onClick={clearUserCache}>ユーザーキャッシュクリア</button>
<button onClick={refreshAllData}>全データ更新</button>
<button onClick={preloadCriticalData}>重要データ先読み</button>
</div>
);
}
パフォーマンス監視:
// React Query Devtoolsを活用
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
function App() {
return (
<QueryClientProvider client={queryClient}>
<YourApp />
{/* 開発環境でのみ表示 */}
{process.env.NODE_ENV === 'development' && (
<ReactQueryDevtools initialIsOpen={false} />
)}
</QueryClientProvider>
);
}
// カスタム監視フック
function useQueryMetrics() {
const queryClient = useQueryClient();
return {
cacheSize: queryClient.getQueryCache().getAll().length,
clearCache: () => queryClient.clear(),
getCacheData: (key) => queryClient.getQueryData(key),
};
}
適切なキャッシュ戦略により、ネットワークリクエストを最小限に抑え、ユーザー体験を大幅に向上させることができます。データの特性を理解し、用途に応じて最適な設定を選択することが重要です。
TanStack Queryをマスターすることで、Reactアプリケーションのデータフェッチング処理が格段に効率化され、保守性の高いコードベースを構築できるようになります。
このトピックはこちらの書籍で勉強するのがおすすめ!
この記事の内容をさらに深く理解したい方におすすめの一冊です。実践的な知識を身につけたい方は、ぜひチェックしてみてください!
おすすめコンテンツ
おすすめVue.js2025/5/31Vue.js 3のComposition API完全ガイド!Reactユーザーでも5分で理解できる実践的な使い方
Vue.js 3の最新機能Composition APIを初心者向けに徹底解説。ReactのHooksとの比較、実際のコード例、よくあるエラーの解決法まで、実務で使える知識を分かりやすく紹介します。
続きを読む React2025/5/12Reactの条件付きレンダリングでのちらつき問題を解決する完全ガイド
ReactでのUI要素がちらつく問題は開発者を悩ませる一般的な課題です。この記事では、useLayoutEffectとuseEffectの適切な使い分け、条件付きレンダリングの最適化など、効果的な解決...
続きを読む React2025/5/12ReactのuseEffect依存配列とイミュータビリティ完全ガイド:よくあるバグと解決策
ReactのuseEffect依存配列とステート更新におけるイミュータビリティの問題を解決する完全ガイド。実際のコード例とベストプラクティスで具体的な問題を解決します。
続きを読む TypeScript2025/6/2TypeScriptでGitHub Actionsカスタムアクション開発完全ガイド!CI/CDワークフローを効率化する実践的な作り方
TypeScriptを使ってGitHub Actionsのカスタムアクションを開発する方法を初心者でも理解できるよう詳しく解説します。実際のコード例とベストプラクティスで、あなたのCI/CDワークフロ...
続きを読む CSS2025/6/2CSS Container Queriesを使った次世代レスポンシブデザイン完全ガイド:2025年最新の実装テクニック
CSS Container Queriesの基本から実践まで完全解説。メディアクエリの限界を超えた次世代レスポンシブデザインの実装方法とコード例を詳しく紹介します。
続きを読む データベース2025/6/2SQLiteとPostgreSQLとMySQL徹底比較!2025年最新版データベース選択の完全ガイド
SQLite、PostgreSQL、MySQLの特徴と選び方を実例と共に解説。パフォーマンス、機能、コスト面から最適なデータベースを選択する方法をプロが教えます。
続きを読む Langchain2025/5/12LangchainとChromaDBで実現する効果的なメモリ管理:実践的トラブルシューティングガイド
Langchainアプリケーションにおけるメモリ管理の問題を解決し、ChromaDBとの統合を最適化するための実践的なガイドです。エラー処理からパフォーマンス改善まで、開発者が直面する一般的な課題に対...
続きを読む GraphQL2025/5/14GraphQLクエリ最適化完全ガイド!パフォーマンス向上のための実践テクニック
GraphQLを使ったアプリケーションのパフォーマンスを向上させるためのクエリ最適化テクニックを初心者にもわかりやすく解説します。N+1問題の解決からキャッシュ戦略まで、実践的なコード例と共に学べます...
続きを読む