tanstack-query について
TanStack Query v5を使うReact開発者向けのパターンガイドです。Suspenseクエリ、キャッシュ管理、ミューテーション、APIサービスの集約方法を扱います。
主な機能
- 01useSuspenseQueryを基本にする
- 02useQueryで状態を扱う
- 03ミューテーションと楽観的更新
- 04キャッシュの無効化・更新・事前取得
- 05APIサービスとクエリキーの集約
活用シーン
- 01新しいコンポーネントのSuspenseクエリ方式の選択
- 02ミューテーション後のキャッシュ更新
- 03再利用可能なAPIリクエストとクエリキーの整理
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ blencorp/ claude-code-kit --skill tanstack-queryインストール後、Claude Code で/tanstack-queryを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
skill-rules-fragment.jsonresources/cache-strategies.mddata-fetching.mdmutation-patterns.md詳細
TanStack Query のパターン
目的
TanStack Query v5 を使用した最新のデータフェッチを紹介します(最新版:5.90.5、2025年11月)。Suspense ベースのクエリ、キャッシュ優先の戦略、一元化された API サービスに重点を置いています。
注:v5(2023年10月リリース)には、v4 からの破壊的変更があります。
- ステータスの
isLoading→isPending cacheTime→gcTime(ガベージコレクション時間)- React 18.0 以降が必須
- useQuery からコールバックを削除(onError、onSuccess、onSettled)
keepPreviousDataをplaceholderData関数に置き換え
このスキルを使う場面
- TanStack Query を使ったデータのフェッチ
- useSuspenseQuery または useQuery の使用
- ミューテーションの管理
- キャッシュの無効化と更新
- API サービスのパターン
クイックスタート
基本パターン:useSuspenseQuery
すべての新しいコンポーネントで、useSuspenseQuery を使用します。
import { useSuspenseQuery } from '@tanstack/react-query';
import { postsApi } from '~/features/posts/api/postsApi';
function PostList() {
const { data: posts } = useSuspenseQuery({
queryKey: ['posts'],
queryFn: postsApi.getAll,
});
return (
<div>
{posts.map(post => (
<PostCard key={post.id} post={post} />
))}
</div>
);
}
// Wrap with Suspense
<Suspense fallback={<PostsSkeleton />}>
<PostList />
</Suspense>
メリット:
isLoadingのチェックが不要- Suspense 境界と連携
- コンポーネントのコードがよりシンプル
- 一貫したローディング UX
useSuspenseQuery のパターン
基本的な使い方
const { data } = useSuspenseQuery({
queryKey: ['user', userId],
queryFn: () => userApi.get(userId),
});
// data is never undefined - guaranteed by Suspense
return <div>{data.name}</div>;
パラメーターを使う場合
function UserPosts({ userId }: { userId: string }) {
const { data: posts } = useSuspenseQuery({
queryKey: ['users', userId, 'posts'],
queryFn: () => postsApi.getByUser(userId),
});
return <div>{posts.length} posts</div>;
}
依存するクエリ
function PostDetails({ postId }: { postId: string }) {
// First query
const { data: post } = useSuspenseQuery({
queryKey: ['posts', postId],
queryFn: () => postsApi.get(postId),
});
// Second query depends on first
const { data: author } = useSuspenseQuery({
queryKey: ['users', post.authorId],
queryFn: () => userApi.get(post.authorId),
});
return <div>{author.name} wrote {post.title}</div>;
}
useQuery(従来のパターン)
コンポーネント内でローディング状態やエラー状態が必要な場合に限り、useQuery を使用します。
import { useQuery } from '@tanstack/react-query';
function Component() {
const { data, isPending, error } = useQuery({
queryKey: ['posts'],
queryFn: postsApi.getAll,
});
if (isPending) return <Spinner />;
if (error) return <Error error={error} />;
return <div>{data.map(...)}</div>;
}
useQuery と useSuspenseQuery の使い分け:
- デフォルトでは
useSuspenseQueryを使用します(推奨)。 - コンポーネントレベルのローディング状態が必要な場合に限り、
useQueryを使用します。 - ほとんどの場合、
useSuspenseQueryと Suspense 境界を使用します。
ミューテーション
基本的なミューテーション
import { useMutation, useQueryClient } from '@tanstack/react-query';
function CreatePostButton() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: postsApi.create,
onSuccess: () => {
// Invalidate and refetch
queryClient.invalidateQueries({ queryKey: ['posts'] });
},
});
const handleCreate = () => {
mutation.mutate({
title: 'New Post',
content: 'Content here',
});
};
return (
<button onClick={handleCreate} disabled={mutation.isPending}>
{mutation.isPending ? 'Creating...' : 'Create Post'}
</button>
);
}
楽観的更新
const mutation = useMutation({
mutationFn: postsApi.update,
onMutate: async (updatedPost) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: ['posts', updatedPost.id] });
// Snapshot previous value
const previousPost = queryClient.getQueryData(['posts', updatedPost.id]);
// Optimistically update
queryClient.setQueryData(['posts', updatedPost.id], updatedPost);
// Return context with snapshot
return { previousPost };
},
onError: (err, updatedPost, context) => {
// Rollback on error
queryClient.setQueryData(
['posts', updatedPost.id],
context.previousPost
);
},
onSettled: (data, error, variables) => {
// Refetch after mutation
queryClient.invalidateQueries({ queryKey: ['posts', variables.id] });
},
});
キャッシュ管理
無効化
import { useQueryClient } from '@tanstack/react-query';
const queryClient = useQueryClient();
// Invalidate all posts queries
queryClient.invalidateQueries({ queryKey: ['posts'] });
// Invalidate specific post
queryClient.invalidateQueries({ queryKey: ['posts', postId] });
// Invalidate all queries
queryClient.invalidateQueries();
手動更新
// Update cache directly
queryClient.setQueryData(['posts', postId], newPost);
// Update with function
queryClient.setQueryData(['posts'], (oldPosts) => [
...oldPosts,
newPost,
]);
プリフェッチ
// Prefetch data
await queryClient.prefetchQuery({
queryKey: ['posts', postId],
queryFn: () => postsApi.get(postId),
});
// In a component
const prefetchPost = (postId: string) => {
queryClient.prefetchQuery({
queryKey: ['posts', postId],
queryFn: () => postsApi.get(postId),
});
};
<Link
to={`/posts/${post.id}`}
onMouseEnter={() => prefetchPost(post.id)}
>
{post.title}
</Link>
API サービスのパターン
一元化された API サービス
// features/posts/api/postsApi.ts
import { apiClient } from '@/lib/apiClient';
import type { Post, CreatePostDto, UpdatePostDto } from '~/types/post';
export const postsApi = {
getAll: async (): Promise<Post[]> => {
const response = await apiClient.get('/posts');
return response.data;
},
get: async (id: string): Promise<Post> => {
const response = await apiClient.get(`/posts/${id}`);
return response.data;
},
create: async (data: CreatePostDto): Promise<Post> => {
const response = await apiClient.post('/posts', data);
return response.data;
},
update: async (id: string, data: UpdatePostDto): Promise<Post> => {
const response = await apiClient.put(`/posts/${id}`, data);
return response.data;
},
delete: async (id: string): Promise<void> => {
await apiClient.delete(`/posts/${id}`);
},
getByUser: async (userId: string): Promise<Post[]> => {
const response = await apiClient.get(`/users/${userId}/posts`);
return response.data;
},
};
コンポーネントでの使用
import { postsApi } from '~/features/posts/api/postsApi';
// In query
const { data } = useSuspenseQuery({
queryKey: ['posts'],
queryFn: postsApi.getAll,
});
// In mutation
const mutation = useMutation({
mutationFn: postsApi.create,
});
クエリキー
キーの構造
// List queries
['posts'] // All posts
['posts', { status: 'published' }] // Filtered posts
// Detail queries
['posts', postId] // Single post
['posts', postId, 'comments'] // Post comments
// Nested resources
['users', userId, 'posts'] // User's posts
['users', userId, 'posts', postId] // Specific user post
キーファクトリー
// features/posts/api/postKeys.ts
export const postKeys = {
all: ['posts'] as const,
lists: () => [...postKeys.all, 'list'] as const,
list: (filters: string) => [...postKeys.lists(), { filters }] as const,
details: () => [...postKeys.all, 'detail'] as const,
detail: (id: string) => [...postKeys.details(), id] as const,
comments: (id: string) => [...postKeys.detail(id), 'comments'] as const,
};
// Usage
const { data } = useSuspenseQuery({
queryKey: postKeys.detail(postId),
queryFn: () => postsApi.get(postId),
});
// Invalidate all post lists
queryClient.invalidateQueries({ queryKey: postKeys.lists() });
エラー処理
エラーバウンダリーを使う場合
import { ErrorBoundary } from 'react-error-boundary';
<ErrorBoundary fallback={<ErrorFallback />}>
<Suspense fallback={<Loading />}>
<DataComponent />
</Suspense>
</ErrorBoundary>
// In component
function DataComponent() {
const { data } = useSuspenseQuery({
queryKey: ['data'],
queryFn: fetchData,
// Errors automatically caught by ErrorBoundary
});
return <div>{data}</div>;
}
リトライとキャッシュの設定
const { data } = useQuery({
queryKey: ['posts'],
queryFn: postsApi.getAll,
retry: 3, // Retry 3 times
retryDelay: 1000, // Wait 1s between retries
gcTime: 5 * 60 * 1000, // Garbage collection time: 5 minutes (v5: was 'cacheTime')
});
ベストプラクティス
1. デフォルトで Suspense を使用する
// ✅ Good: useSuspenseQuery + Suspense
<Suspense fallback={<Skeleton />}>
<DataComponent />
</Suspense>
function DataComponent() {
const { data } = useSuspenseQuery({...});
return <div>{data}</div>;
}
// ❌ Avoid: useQuery with manual loading
function DataComponent() {
const { data, isPending } = useQuery({...});
if (isPending) return <Spinner />;
return <div>{data}</div>;
}
2. 一貫性のあるクエリキー
// ✅ Good: Use key factories
const { data } = useSuspenseQuery({
queryKey: postKeys.detail(id),
queryFn: () => postsApi.get(id),
});
// ❌ Avoid: Inconsistent keys
const { data } = useSuspenseQuery({
queryKey: ['post', id], // Different format
queryFn: () => postsApi.get(id),
});
3. 一元化された API サービス
// ✅ Good: API service
const { data } = useSuspenseQuery({
queryKey: ['posts'],
queryFn: postsApi.getAll,
});
// ❌ Avoid: Inline fetching
const { data } = useSuspenseQuery({
queryKey: ['posts'],
queryFn: async () => {
const res = await fetch('/api/posts');
return res.json();
},
});
その他のリソース
その他のパターンについては、以下を参照してください。
- data-fetching.md - 高度なパターン
- cache-strategies.md - キャッシュ管理
- mutation-patterns.md - 複雑なミューテーション
コメント
Markdown 対応