tanstack-query

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

TanStack Query Patterns

TanStack Query 模式

Purpose

目的

Modern data fetching with TanStack Query v5 (latest: 5.90.5, November 2025), emphasizing Suspense-based queries, cache-first strategies, and centralized API services.
Note: v5 (released October 2023) has breaking changes from v4:
  • isLoading
    isPending
    for status
  • cacheTime
    gcTime
    (garbage collection time)
  • React 18.0+ required
  • Callbacks removed from useQuery (onError, onSuccess, onSettled)
  • keepPreviousData
    replaced with
    placeholderData
    function
使用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
    函数替代

When to Use This Skill

适用场景

  • Fetching data with TanStack Query
  • Using useSuspenseQuery or useQuery
  • Managing mutations
  • Cache invalidation and updates
  • API service patterns

  • 使用TanStack Query获取数据
  • 使用useSuspenseQuery或useQuery
  • 管理Mutation操作
  • 缓存失效与更新
  • API服务模式设计

Quick Start

快速开始

Primary Pattern: useSuspenseQuery

核心模式:useSuspenseQuery

For all new components, use
useSuspenseQuery
:
typescript
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>
Benefits:
  • No
    isLoading
    checks needed
  • Integrates with Suspense boundaries
  • Cleaner component code
  • Consistent loading UX

对于所有新组件,优先使用
useSuspenseQuery
typescript
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>
  );
}

// 用Suspense包裹
<Suspense fallback={<PostsSkeleton />}>
  <PostList />
</Suspense>
优势:
  • 无需检查
    isLoading
    状态
  • 与Suspense边界集成
  • 组件代码更简洁
  • 加载体验一致

useSuspenseQuery Patterns

useSuspenseQuery 模式

Basic Usage

基础用法

typescript
const { data } = useSuspenseQuery({
  queryKey: ['user', userId],
  queryFn: () => userApi.get(userId),
});

// data is never undefined - guaranteed by Suspense
return <div>{data.name}</div>;
typescript
const { data } = useSuspenseQuery({
  queryKey: ['user', userId],
  queryFn: () => userApi.get(userId),
});

// 借助Suspense,data永远不会为undefined
return <div>{data.name}</div>;

With Parameters

带参数用法

typescript
function UserPosts({ userId }: { userId: string }) {
  const { data: posts } = useSuspenseQuery({
    queryKey: ['users', userId, 'posts'],
    queryFn: () => postsApi.getByUser(userId),
  });

  return <div>{posts.length} posts</div>;
}
typescript
function UserPosts({ userId }: { userId: string }) {
  const { data: posts } = useSuspenseQuery({
    queryKey: ['users', userId, 'posts'],
    queryFn: () => postsApi.getByUser(userId),
  });

  return <div>{posts.length} posts</div>;
}

Dependent Queries

依赖查询

typescript
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>;
}

typescript
function PostDetails({ postId }: { postId: string }) {
  // 第一个查询
  const { data: post } = useSuspenseQuery({
    queryKey: ['posts', postId],
    queryFn: () => postsApi.get(postId),
  });

  // 第二个查询依赖第一个查询的结果
  const { data: author } = useSuspenseQuery({
    queryKey: ['users', post.authorId],
    queryFn: () => userApi.get(post.authorId),
  });

  return <div>{author.name} wrote {post.title}</div>;
}

useQuery (Legacy Pattern)

useQuery(遗留模式)

Use
useQuery
only when you need loading/error states in the component:
typescript
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>;
}
When to use
useQuery
vs
useSuspenseQuery
:
  • Use
    useSuspenseQuery
    by default (preferred)
  • Use
    useQuery
    only when you need component-level loading states
  • Most cases should use
    useSuspenseQuery
    + Suspense boundaries

仅当需要在组件内处理加载/错误状态时,才使用
useQuery
typescript
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边界

Mutations

Mutation 操作

Basic Mutation

基础Mutation

typescript
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>
  );
}
typescript
import { useMutation, useQueryClient } from '@tanstack/react-query';

function CreatePostButton() {
  const queryClient = useQueryClient();

  const mutation = useMutation({
    mutationFn: postsApi.create,
    onSuccess: () => {
      // 失效并重新获取数据
      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>
  );
}

Optimistic Updates

乐观更新

typescript
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] });
  },
});

typescript
const mutation = useMutation({
  mutationFn: postsApi.update,
  onMutate: async (updatedPost) => {
    // 取消正在进行的重新获取请求
    await queryClient.cancelQueries({ queryKey: ['posts', updatedPost.id] });

    // 快照之前的值
    const previousPost = queryClient.getQueryData(['posts', updatedPost.id]);

    // 执行乐观更新
    queryClient.setQueryData(['posts', updatedPost.id], updatedPost);

    // 返回包含快照的上下文
    return { previousPost };
  },
  onError: (err, updatedPost, context) => {
    // 错误时回滚
    queryClient.setQueryData(
      ['posts', updatedPost.id],
      context.previousPost
    );
  },
  onSettled: (data, error, variables) => {
    // Mutation完成后重新获取数据
    queryClient.invalidateQueries({ queryKey: ['posts', variables.id] });
  },
});

Cache Management

缓存管理

Invalidation

缓存失效

typescript
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();
typescript
import { useQueryClient } from '@tanstack/react-query';

const queryClient = useQueryClient();

// 使所有posts相关查询失效
queryClient.invalidateQueries({ queryKey: ['posts'] });

// 使特定post查询失效
queryClient.invalidateQueries({ queryKey: ['posts', postId] });

// 使所有查询失效
queryClient.invalidateQueries();

Manual Updates

手动更新缓存

typescript
// Update cache directly
queryClient.setQueryData(['posts', postId], newPost);

// Update with function
queryClient.setQueryData(['posts'], (oldPosts) => [
  ...oldPosts,
  newPost,
]);
typescript
// 直接更新缓存
queryClient.setQueryData(['posts', postId], newPost);

// 通过函数更新缓存
queryClient.setQueryData(['posts'], (oldPosts) => [
  ...oldPosts,
  newPost,
]);

Prefetching

预获取数据

typescript
// 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>

typescript
// 预获取数据
await queryClient.prefetchQuery({
  queryKey: ['posts', postId],
  queryFn: () => postsApi.get(postId),
});

// 在组件中使用
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 Service Pattern

API服务模式

Centralized API Service

集中式API服务

typescript
// 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;
  },
};
typescript
// 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;
  },
};

Usage in Components

在组件中使用

typescript
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,
});

typescript
import { postsApi } from '~/features/posts/api/postsApi';

// 在查询中使用
const { data } = useSuspenseQuery({
  queryKey: ['posts'],
  queryFn: postsApi.getAll,
});

// 在Mutation中使用
const mutation = useMutation({
  mutationFn: postsApi.create,
});

Query Keys

查询键(Query Keys)

Key Structure

键结构

typescript
// 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
typescript
// 列表查询
['posts']                          // 所有文章
['posts', { status: 'published' }] // 已发布的文章

// 详情查询
['posts', postId]                  // 单篇文章
['posts', postId, 'comments']      // 文章评论

// 嵌套资源
['users', userId, 'posts']         // 用户的文章
['users', userId, 'posts', postId] // 用户的特定文章

Key Factories

查询键工厂

typescript
// 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() });

typescript
// 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,
};

// 使用示例
const { data } = useSuspenseQuery({
  queryKey: postKeys.detail(postId),
  queryFn: () => postsApi.get(postId),
});

// 使所有文章列表查询失效
queryClient.invalidateQueries({ queryKey: postKeys.lists() });

Error Handling

错误处理

With Error Boundaries

结合错误边界

typescript
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>;
}
typescript
import { ErrorBoundary } from 'react-error-boundary';

<ErrorBoundary fallback={<ErrorFallback />}>
  <Suspense fallback={<Loading />}>
    <DataComponent />
  </Suspense>
</ErrorBoundary>

// 在组件中
function DataComponent() {
  const { data } = useSuspenseQuery({
    queryKey: ['data'],
    queryFn: fetchData,
    // 错误会被ErrorBoundary自动捕获
  });

  return <div>{data}</div>;
}

Retry and Cache Configuration

重试与缓存配置

typescript
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')
});

typescript
const { data } = useQuery({
  queryKey: ['posts'],
  queryFn: postsApi.getAll,
  retry: 3,              // 重试3次
  retryDelay: 1000,      // 重试间隔1秒
  gcTime: 5 * 60 * 1000, // 垃圾回收时间:5分钟(v5版本中替代原`cacheTime`)
});

Best Practices

最佳实践

1. Use Suspense by Default

1. 默认使用Suspense

typescript
// ✅ 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>;
}
typescript
// ✅ 推荐:useSuspenseQuery + Suspense
<Suspense fallback={<Skeleton />}>
  <DataComponent />
</Suspense>

function DataComponent() {
  const { data } = useSuspenseQuery({...});
  return <div>{data}</div>;
}

// ❌ 不推荐:使用useQuery手动处理加载状态
function DataComponent() {
  const { data, isPending } = useQuery({...});
  if (isPending) return <Spinner />;
  return <div>{data}</div>;
}

2. Consistent Query Keys

2. 保持查询键一致

typescript
// ✅ 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),
});
typescript
// ✅ 推荐:使用键工厂
const { data } = useSuspenseQuery({
  queryKey: postKeys.detail(id),
  queryFn: () => postsApi.get(id),
});

// ❌ 不推荐:键格式不一致
const { data } = useSuspenseQuery({
  queryKey: ['post', id], // 格式不同
  queryFn: () => postsApi.get(id),
});

3. Centralized API Services

3. 使用集中式API服务

typescript
// ✅ 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();
  },
});

typescript
// ✅ 推荐:使用API服务
const { data } = useSuspenseQuery({
  queryKey: ['posts'],
  queryFn: postsApi.getAll,
});

// ❌ 不推荐:内联请求
const { data } = useSuspenseQuery({
  queryKey: ['posts'],
  queryFn: async () => {
    const res = await fetch('/api/posts');
    return res.json();
  },
});

Additional Resources

更多资源

For more patterns, see:
  • data-fetching.md - Advanced patterns
  • cache-strategies.md - Cache management
  • mutation-patterns.md - Complex mutations
如需了解更多模式,请查看:
  • data-fetching.md - 高级模式
  • cache-strategies.md - 缓存管理
  • mutation-patterns.md - 复杂Mutation操作