mcpField
T
tanstack-query

tanstack-query

github.com/blencorp開発11192 回インストール49 週間前に更新
リポジトリ

tanstack-query について

TanStack Query v5を使うReact開発者向けのパターンガイドです。Suspenseクエリ、キャッシュ管理、ミューテーション、APIサービスの集約方法を扱います。

主な機能

  1. 01useSuspenseQueryを基本にする
  2. 02useQueryで状態を扱う
  3. 03ミューテーションと楽観的更新
  4. 04キャッシュの無効化・更新・事前取得
  5. 05APIサービスとクエリキーの集約

活用シーン

  1. 01新しいコンポーネントのSuspenseクエリ方式の選択
  2. 02ミューテーション後のキャッシュ更新
  3. 03再利用可能なAPIリクエストとクエリキーの整理

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/blencorp/claude-code-kit --skill tanstack-query

インストール後、Claude Code で/tanstack-queryを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

skill-rules-fragment.json
resources/
cache-strategies.md
data-fetching.md
mutation-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();
  },
});

その他のリソース

その他のパターンについては、以下を参照してください。

コメント