tanstack-query について
TypeScript アプリで TanStack Query v5 を使い、サーバー状態の取得とキャッシュを扱うスキルです。型付きクエリ、キャッシュ管理、ミューテーションに加え、旧 v4 API の回避方法を説明します。
主な機能
- 01安定した階層型クエリキー
- 02再利用可能な型付き queryOptions
- 03依存クエリと並列クエリ
- 04ミューテーションとキャッシュ無効化
- 05Suspense とページネーション対応クエリ
- 06v5 への移行ガイド
活用シーン
- 01複数コンポーネント間でクエリ定義を整理
- 02依存データや並列データの取得
- 03書き込み後のキャッシュ整合性の維持
- 04ページ単位のデータ読み込みの実装
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ mindrally/ skills --skill tanstack-queryインストール後、Claude Code で/tanstack-queryを入力して読み込まれたか確認します。
詳細
TanStack Query のベストプラクティス
TanStack Query(以前の React Query)は、サーバー状態のキャッシュ、バックグラウンド更新、古いデータの管理を標準で処理します。このスキルでは v5 のパターンと API を扱います。v5 では v4 からいくつかの破壊的変更が導入されており、オンラインに残っている古いサンプルには反映されていません。
基本原則
- すべてのサーバー状態管理とデータ取得に TanStack Query を使用します。これは汎用的なクライアント状態管理ツールではありません。クライアントのみの状態は、代わりに
useState/context/状態管理ライブラリで管理します - サーバーデータに対する
useEffectとuseStateの使用を最小限に抑え、TanStack Query に組み込まれた状態管理を優先します - すべてのクエリには、保持するデータを一意に表す、安定したシリアライズ可能なクエリキーが必要です
- ミューテーションは書き込みを、クエリは読み取りを処理します。この境界を曖昧にしないでください
- ユーザーにわかりやすいメッセージを使って、適切なエラー処理を実装します
- クエリのレスポンスに対する完全な型安全性を確保するため、TypeScript を使用します
注意すべき v5 の破壊的変更
以下の v4 パターンを見つけた場合、または記述する場合は、更新してください。
- オブジェクト構文のみ:
useQuery、useInfiniteQueryなどは、位置引数(useQuery(key, fn, options))を受け付けなくなりました。常に単一のオプションオブジェクトを渡してください:useQuery({ queryKey, queryFn, ...options })。 isPendingはisLoadingに代わる名前です:useMutationで「データがまだなく、フェッチが進行中」を示します。useQueryでは、isPendingはキャッシュされたデータがまったく存在しないことを意味します。isLoadingは現在、isPending && isFetchingから導出され、従来の「初回読み込み」スピナーの場合にも引き続き使用できます。cacheTimeはgcTimeに名称変更されました(ガベージコレクション時間)。queryOptions()ヘルパーを使うと、コンポーネント、ローダー、プリフェッチ呼び出しで共有する再利用可能な型付きクエリ定義を作成できます。- Suspense ベースのデータ取得には
useSuspenseQuery(およびuseSuspenseInfiniteQuery)を使用します。これは以前のsuspense: trueオプションに代わるものです。 placeholderData: keepPreviousDataは、ページネーションで使われていた以前のkeepPreviousData: trueというブール値に代わるものです。
プロジェクト構成
src/
api/
client.ts # API client configuration
endpoints/
users.ts # User-related API calls
posts.ts # Post-related API calls
queries/
postKeys.ts # Query key factory
postQueryOptions.ts # queryOptions() definitions
hooks/
queries/
useUsers.ts # User query hooks
usePosts.ts # Post query hooks
mutations/
useCreateUser.ts # User mutation hooks
providers/
QueryProvider.tsx # Query client provider setup
types/
api.ts # API response types
セットアップと設定
Query Client の設定
// providers/QueryProvider.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 minutes; defaults to 0 (always stale) if unset
gcTime: 1000 * 60 * 30, // 30 minutes (formerly cacheTime)
retry: (failureCount, error: any) => error?.status !== 404 && failureCount < 3,
refetchOnWindowFocus: true,
refetchOnReconnect: true,
},
mutations: {
retry: 1,
},
},
});
export function QueryProvider({ children }: { children: React.ReactNode }) {
return (
<QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
QueryClient はアプリのルートで一度だけインスタンス化します。コンポーネント内では決してインスタンス化しないでください。そうしないと、レンダーのたびにキャッシュがリセットされます。
クエリのベストプラクティス
1. クエリキーの構成
効率的にキャッシュを管理できるよう、一貫性のある階層型のクエリキーを使用します。
// Query key factory pattern
export const userKeys = {
all: ['users'] as const,
lists: () => [...userKeys.all, 'list'] as const,
list: (filters: UserFilters) => [...userKeys.lists(), filters] as const,
details: () => [...userKeys.all, 'detail'] as const,
detail: (id: string) => [...userKeys.details(), id] as const,
};
2. queryOptions ヘルパー(v5)
queryOptions() でクエリを一度定義し、コンポーネント、ルーターローダー、プリフェッチ呼び出しで同じ定義を再利用します。クエリキー、クエリ関数、オプションを重複して記述せず、一か所にまとめられます。
// queries/postQueryOptions.ts
import { queryOptions } from '@tanstack/react-query';
import { postKeys } from './postKeys';
import { fetchPost } from '@/api/endpoints/posts';
export const postQueryOptions = (id: string) =>
queryOptions({
queryKey: postKeys.detail(id),
queryFn: () => fetchPost(id),
staleTime: 1000 * 60 * 5,
});
// In a component
const { data } = useQuery(postQueryOptions(postId));
// In a TanStack Router loader — eliminates loading spinners on navigation
export const Route = createFileRoute('/posts/$postId')({
loader: ({ params, context: { queryClient } }) =>
queryClient.ensureQueryData(postQueryOptions(params.postId)),
});
定義を共有してプリフェッチできるよう、queryOptions は必ずコンポーネントの外で定義してください。useQuery() を呼び出すたびに新しいオブジェクトリテラルをインラインで作成しないでください。
3. カスタムクエリフック
queryOptions() ファクトリーをほかで再利用しない場合は、再利用可能な型付きクエリフックを作成します。
// hooks/queries/useUser.ts
import { useQuery, UseQueryOptions } from '@tanstack/react-query';
import { userKeys } from '@/api/queryKeys';
import { getUser, User } from '@/api/endpoints/users';
export function useUser(
userId: string,
options?: Omit<UseQueryOptions<User, Error>, 'queryKey' | 'queryFn'>
) {
return useQuery({
queryKey: userKeys.detail(userId),
queryFn: () => getUser(userId),
enabled: !!userId,
...options,
});
}
4. 依存クエリ
ほかのデータに依存するクエリを処理します。
function useUserPosts(userId: string) {
const { data: user } = useUser(userId);
return useQuery({
queryKey: ['posts', { userId }],
queryFn: () => fetchUserPosts(userId),
enabled: !!user, // Only run when user data is available
});
}
5. 並列クエリ
複数のリソースを同時に取得します。
import { useQueries } from '@tanstack/react-query';
function useMultipleUsers(userIds: string[]) {
return useQueries({
queries: userIds.map((id) => ({
queryKey: userKeys.detail(id),
queryFn: () => getUser(id),
})),
});
}
ミューテーションのベストプラクティス
1. 基本的なミューテーション
const { mutate, mutateAsync, isPending } = useMutation({
mutationFn: (input: CreatePostInput) => createPost(input),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: postKeys.lists() });
toast.success('Post created!');
},
onError: (error) => {
toast.error(error.message);
},
});
// Usage
mutate({ title: 'Hello', body: '...' });
isPending は「ミューテーションが進行中」を示す v5 の名前です(v4 ではミューテーションにも isLoading が使われていましたが、この名前は廃止されました)。
2. 楽観的更新
ミューテーションの進行中に、すぐにフィードバックを提供します。
import { useMutation, useQueryClient } from '@tanstack/react-query';
function useUpdateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: updateUser,
onMutate: async (newUser) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: userKeys.detail(newUser.id) });
// Snapshot previous value
const previousUser = queryClient.getQueryData(userKeys.detail(newUser.id));
// Optimistically update
queryClient.setQueryData(userKeys.detail(newUser.id), newUser);
return { previousUser };
},
onError: (err, newUser, context) => {
// Rollback on error
queryClient.setQueryData(
userKeys.detail(newUser.id),
context?.previousUser
);
},
onSettled: (data, error, variables) => {
// Refetch after error or success
queryClient.invalidateQueries({ queryKey: userKeys.detail(variables.id) });
},
});
}
3. キャッシュの無効化
ミューテーション後に、関連するクエリを適切に無効化します。
function useDeleteUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: deleteUser,
onSuccess: () => {
// Invalidate all user-related queries
queryClient.invalidateQueries({ queryKey: userKeys.all });
},
});
}
知っておくと便利なその他のキャッシュ操作:
// Remove from cache entirely (not just marked stale)
queryClient.removeQueries({ queryKey: userKeys.detail(id) });
// Directly write to the cache without a refetch
queryClient.setQueryData(userKeys.detail(id), newData);
エラー処理
1. グローバルエラーハンドラー
const queryClient = new QueryClient({
defaultOptions: {
queries: {
throwOnError: false,
},
mutations: {
onError: (error) => {
// Global error handling (e.g., toast notification)
toast.error(error.message);
},
},
},
});
2. コンポーネントレベルのエラー処理
function UserProfile({ userId }: { userId: string }) {
const { data, error, isPending, isError } = useUser(userId);
if (isPending) return <Skeleton />;
if (isError) return <ErrorMessage error={error} />;
return <UserCard user={data} />;
}
3. 条件付き再試行ロジック
404 など、再試行しても成功しないエラーは再試行をスキップします。
retry: (failureCount, error) => {
if (error.status === 404) return false;
return failureCount < 3;
},
4. Suspense モード(v5)
古い suspense: true オプションの代わりに、Suspense ベースのデータ取得には useSuspenseQuery を使用します。data が undefined になることがないため、戻り値の型もより絞り込まれます。
import { useSuspenseQuery } from '@tanstack/react-query';
import { ErrorBoundary } from 'react-error-boundary';
import { Suspense } from 'react';
function UserProfile({ userId }: { userId: string }) {
// No need to check isPending — Suspense handles the loading state
const { data } = useSuspenseQuery(userQueryOptions(userId));
return <UserCard user={data} />;
}
function App() {
return (
<ErrorBoundary fallback={<ErrorFallback />}>
<Suspense fallback={<Loading />}>
<UserProfile userId="123" />
</Suspense>
</ErrorBoundary>
);
}
Suspense に切り替えずに、エラーを最も近い ErrorBoundary に伝播させたい場合は、通常の useQuery で throwOnError: true を使用します。
パフォーマンスの最適化
1. データの選択と変換
必要なデータのみを購読します。
function useUserName(userId: string) {
return useUser(userId, {
select: (user) => user.name,
});
}
2. プリフェッチ
必要になる前にデータをプリフェッチします。ホバー時やルーティング中に実行できます。
function UserList() {
const queryClient = useQueryClient();
const prefetchUser = (userId: string) => {
queryClient.prefetchQuery(userQueryOptions(userId));
};
return (
<ul>
{users.map((user) => (
<li key={user.id} onMouseEnter={() => prefetchUser(user.id)}>
{user.name}
</li>
))}
</ul>
);
}
3. 無限クエリ
ページ分割されたデータを効率的に処理します。
import { useInfiniteQuery } from '@tanstack/react-query';
function useInfinitePosts() {
return useInfiniteQuery({
queryKey: postKeys.lists(),
queryFn: ({ pageParam }) => fetchPosts(pageParam),
initialPageParam: 0,
getNextPageParam: (lastPage) => lastPage.nextCursor,
getPreviousPageParam: (firstPage) => firstPage.prevCursor,
});
}
// data.pages is an array of page results — flatten for rendering
const allPosts = data?.pages.flatMap((page) => page.items) ?? [];
ページ分割またはフィルター付きのクエリでは、placeholderData: keepPreviousData(@tanstack/react-query からインポート)を使用すると、次のページの読み込み中も前のページのデータを表示し続けられ、読み込み状態へのちらつきを防げます。
import { keepPreviousData, useQuery } from '@tanstack/react-query';
useQuery({
queryKey: postKeys.list({ page }),
queryFn: () => fetchPosts({ page }),
placeholderData: keepPreviousData,
});
notifyOnChangeProps を使用して、コンポーネントが実際に読み取る特定の結果プロパティだけに再レンダリングを制限します。
TypeScript のヒント
queryFnの戻り値には必ず明示的に型を指定するか、型指定された API 関数から推論させます- フックの戻り値の型には
QueryObserverResult<TData, TError>を使用します - ミューテーションには
UseMutationResult<TData, TError, TVariables>を使用します
主な規約
- 機能ベースの構成:クエリフックと
queryOptionsファクトリーを、機能ごとのディレクトリにまとめます - 一貫したクエリキー:型安全で整理されたキーには、ファクトリー関数を使用します
- 再利用可能なクエリには
queryOptionsを使用:クエリが複数の場所(コンポーネント、ローダー、プリフェッチ)で使用される場合は、その場しのぎのインラインオプションよりqueryOptions()を優先します - 型安全性:すべての API レスポンスに TypeScript のインターフェースを定義します
- DevTools:開発時には必ず React Query DevTools を含めます
- 深くネストされたクエリを避ける:可能な場合はクエリ構造をフラット化します
- 必要なデータのみを取得する:API パラメーターを使用してレスポンスのサイズを制限します
- 読み込み状態とエラー状態を処理する:適切な UI フィードバックを必ず提供します
避けるべきアンチパターン
- データ取得に
useEffectを使用しないでください。代わりにクエリまたはルーターローダーを使用します - サーバー状態をローカル状態(
useState)に保存しないでください useQuery/useInfiniteQueryに位置引数を渡さないでください。v5 ではオプションオブジェクト形式が必須です- ミューテーションで
isLoadingだけを確認しないでください。isPendingを使用します - 読み込み状態とエラー状態の処理を忘れないでください
- キャッシュを再利用できなくなるほど具体的なクエリキーを作成しないでください
- ミューテーション後のキャッシュ無効化を省略しないでください
- 条件付きクエリで
enabledオプションを無視しないでください - 別の場所でも再利用する
queryOptions/クエリ設定をコンポーネント内にインラインで定義しないでください。関連する場所にまとめて共有します