tanstack-query

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

TanStack Query Best Practices

TanStack Query 最佳实践

You are an expert in TanStack Query (formerly React Query), TypeScript, and React development. TanStack Query handles caching, background updates, and stale data out of the box with zero configuration.
您是TanStack Query(前身为React Query)、TypeScript和React开发领域的专家。TanStack Query无需配置即可开箱即用,处理缓存、后台更新和过期数据。

Core Principles

核心原则

  • Use TanStack Query for all server state management and data fetching
  • Minimize the use of
    useEffect
    and
    useState
    for server data; favor TanStack Query's built-in state management
  • Implement proper error handling with user-friendly messages
  • Use TypeScript for full type safety with query responses
  • 使用TanStack Query处理所有服务端状态管理和数据获取
  • 尽量减少使用
    useEffect
    useState
    存储服务端数据;优先使用TanStack Query内置的状态管理
  • 实现带有友好用户提示的错误处理机制
  • 使用TypeScript确保查询响应的类型安全

Project Structure

项目结构

src/
  api/
    client.ts             # API client configuration
    endpoints/
      users.ts            # User-related API calls
      posts.ts            # Post-related API calls
  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
src/
  api/
    client.ts             # API客户端配置
    endpoints/
      users.ts            # 用户相关API调用
      posts.ts            # 文章相关API调用
  hooks/
    queries/
      useUsers.ts         # 用户查询钩子
      usePosts.ts         # 文章查询钩子
    mutations/
      useCreateUser.ts    # 用户修改钩子
  providers/
    QueryProvider.tsx     # Query客户端提供者配置
  types/
    api.ts                # API响应类型定义

Setup and Configuration

配置与设置

Query Client Configuration

Query客户端配置

typescript
// 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
      gcTime: 1000 * 60 * 30,   // 30 minutes (formerly cacheTime)
      retry: 3,
      refetchOnWindowFocus: true,
      refetchOnReconnect: true,
    },
    mutations: {
      retry: 1,
    },
  },
});

export function QueryProvider({ children }: { children: React.ReactNode }) {
  return (
    <QueryClientProvider client={queryClient}>
      {children}
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  );
}
typescript
// 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分钟
      gcTime: 1000 * 60 * 30,   // 30分钟(原cacheTime)
      retry: 3,
      refetchOnWindowFocus: true,
      refetchOnReconnect: true,
    },
    mutations: {
      retry: 1,
    },
  },
});

export function QueryProvider({ children }: { children: React.ReactNode }) {
  return (
    <QueryClientProvider client={queryClient}>
      {children}
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  );
}

Query Best Practices

查询最佳实践

1. Query Key Organization

1. 查询键组织

Use consistent, hierarchical query keys for efficient cache management:
typescript
// 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,
};
使用一致的层级化查询键,实现高效的缓存管理:
typescript
// 查询键工厂模式
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. Custom Query Hooks

2. 自定义查询钩子

Create reusable, typed query hooks:
typescript
// 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,
  });
}
创建可复用的类型化查询钩子:
typescript
// 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,
  });
}

3. Dependent Queries

3. 依赖查询

Handle queries that depend on other data:
typescript
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
  });
}
处理依赖其他数据的查询:
typescript
function useUserPosts(userId: string) {
  const { data: user } = useUser(userId);

  return useQuery({
    queryKey: ['posts', { userId }],
    queryFn: () => fetchUserPosts(userId),
    enabled: !!user, // 仅当用户数据可用时执行
  });
}

4. Parallel Queries

4. 并行查询

Fetch multiple resources simultaneously:
typescript
import { useQueries } from '@tanstack/react-query';

function useMultipleUsers(userIds: string[]) {
  return useQueries({
    queries: userIds.map((id) => ({
      queryKey: userKeys.detail(id),
      queryFn: () => getUser(id),
    })),
  });
}
同时获取多个资源:
typescript
import { useQueries } from '@tanstack/react-query';

function useMultipleUsers(userIds: string[]) {
  return useQueries({
    queries: userIds.map((id) => ({
      queryKey: userKeys.detail(id),
      queryFn: () => getUser(id),
    })),
  });
}

Mutation Best Practices

修改(Mutation)最佳实践

1. Optimistic Updates

1. 乐观更新

Provide instant feedback while mutations are in flight:
typescript
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) });
    },
  });
}
在修改请求处理过程中提供即时反馈:
typescript
import { useMutation, useQueryClient } from '@tanstack/react-query';

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

  return useMutation({
    mutationFn: updateUser,
    onMutate: async (newUser) => {
      // 取消正在进行的重新获取请求
      await queryClient.cancelQueries({ queryKey: userKeys.detail(newUser.id) });

      // 快照之前的值
      const previousUser = queryClient.getQueryData(userKeys.detail(newUser.id));

      // 执行乐观更新
      queryClient.setQueryData(userKeys.detail(newUser.id), newUser);

      return { previousUser };
    },
    onError: (err, newUser, context) => {
      // 出错时回滚
      queryClient.setQueryData(
        userKeys.detail(newUser.id),
        context?.previousUser
      );
    },
    onSettled: (data, error, variables) => {
      // 无论成功或失败都重新获取数据
      queryClient.invalidateQueries({ queryKey: userKeys.detail(variables.id) });
    },
  });
}

2. Cache Invalidation

2. 缓存失效

Properly invalidate related queries after mutations:
typescript
function useDeleteUser() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: deleteUser,
    onSuccess: () => {
      // Invalidate all user-related queries
      queryClient.invalidateQueries({ queryKey: userKeys.all });
    },
  });
}
修改后正确使相关查询失效:
typescript
function useDeleteUser() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: deleteUser,
    onSuccess: () => {
      // 使所有用户相关查询失效
      queryClient.invalidateQueries({ queryKey: userKeys.all });
    },
  });
}

Error Handling

错误处理

1. Global Error Handler

1. 全局错误处理器

typescript
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      throwOnError: false,
    },
    mutations: {
      onError: (error) => {
        // Global error handling (e.g., toast notification)
        toast.error(error.message);
      },
    },
  },
});
typescript
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      throwOnError: false,
    },
    mutations: {
      onError: (error) => {
        // 全局错误处理(如:弹出提示框)
        toast.error(error.message);
      },
    },
  },
});

2. Component-Level Error Handling

2. 组件级错误处理

typescript
function UserProfile({ userId }: { userId: string }) {
  const { data, error, isLoading, isError } = useUser(userId);

  if (isLoading) return <Skeleton />;
  if (isError) return <ErrorMessage error={error} />;

  return <UserCard user={data} />;
}
typescript
function UserProfile({ userId }: { userId: string }) {
  const { data, error, isLoading, isError } = useUser(userId);

  if (isLoading) return <Skeleton />;
  if (isError) return <ErrorMessage error={error} />;

  return <UserCard user={data} />;
}

3. Error Boundaries with Suspense

3. 结合Suspense的错误边界

typescript
import { ErrorBoundary } from 'react-error-boundary';
import { Suspense } from 'react';

function App() {
  return (
    <ErrorBoundary fallback={<ErrorFallback />}>
      <Suspense fallback={<Loading />}>
        <UserProfile userId="123" />
      </Suspense>
    </ErrorBoundary>
  );
}
typescript
import { ErrorBoundary } from 'react-error-boundary';
import { Suspense } from 'react';

function App() {
  return (
    <ErrorBoundary fallback={<ErrorFallback />}>
      <Suspense fallback={<Loading />}>
        <UserProfile userId="123" />
      </Suspense>
    </ErrorBoundary>
  );
}

Performance Optimization

性能优化

1. Select and Transform Data

1. 选择与转换数据

Only subscribe to the data you need:
typescript
function useUserName(userId: string) {
  return useUser(userId, {
    select: (user) => user.name,
  });
}
仅订阅您需要的数据:
typescript
function useUserName(userId: string) {
  return useUser(userId, {
    select: (user) => user.name,
  });
}

2. Prefetching

2. 预获取数据

Prefetch data before it's needed:
typescript
function UserList() {
  const queryClient = useQueryClient();

  const prefetchUser = (userId: string) => {
    queryClient.prefetchQuery({
      queryKey: userKeys.detail(userId),
      queryFn: () => getUser(userId),
      staleTime: 1000 * 60 * 5,
    });
  };

  return (
    <ul>
      {users.map((user) => (
        <li key={user.id} onMouseEnter={() => prefetchUser(user.id)}>
          {user.name}
        </li>
      ))}
    </ul>
  );
}
在需要之前预获取数据:
typescript
function UserList() {
  const queryClient = useQueryClient();

  const prefetchUser = (userId: string) => {
    queryClient.prefetchQuery({
      queryKey: userKeys.detail(userId),
      queryFn: () => getUser(userId),
      staleTime: 1000 * 60 * 5,
    });
  };

  return (
    <ul>
      {users.map((user) => (
        <li key={user.id} onMouseEnter={() => prefetchUser(user.id)}>
          {user.name}
        </li>
      ))}
    </ul>
  );
}

3. Infinite Queries

3. 无限查询

Handle paginated data efficiently:
typescript
import { useInfiniteQuery } from '@tanstack/react-query';

function useInfinitePosts() {
  return useInfiniteQuery({
    queryKey: ['posts'],
    queryFn: ({ pageParam }) => fetchPosts(pageParam),
    initialPageParam: 0,
    getNextPageParam: (lastPage) => lastPage.nextCursor,
    getPreviousPageParam: (firstPage) => firstPage.prevCursor,
  });
}
高效处理分页数据:
typescript
import { useInfiniteQuery } from '@tanstack/react-query';

function useInfinitePosts() {
  return useInfiniteQuery({
    queryKey: ['posts'],
    queryFn: ({ pageParam }) => fetchPosts(pageParam),
    initialPageParam: 0,
    getNextPageParam: (lastPage) => lastPage.nextCursor,
    getPreviousPageParam: (firstPage) => firstPage.prevCursor,
  });
}

Key Conventions

关键约定

  1. Feature-based organization: Group query hooks within feature-specific directories
  2. Consistent query keys: Use factory functions for type-safe, organized keys
  3. Type safety: Define TypeScript interfaces for all API responses
  4. DevTools: Always include React Query DevTools in development
  5. Avoid deeply nested queries: Flatten query structures when possible
  6. Fetch only needed data: Use API parameters to limit response size
  7. Handle loading and error states: Always provide appropriate UI feedback
  1. 基于功能的组织方式:将查询钩子按功能分组到特定目录中
  2. 一致的查询键:使用工厂函数创建类型安全、结构清晰的查询键
  3. 类型安全:为所有API响应定义TypeScript接口
  4. 开发工具:开发环境中始终包含React Query DevTools
  5. 避免深层嵌套查询:尽可能扁平化查询结构
  6. 仅获取所需数据:使用API参数限制响应大小
  7. 处理加载和错误状态:始终提供合适的UI反馈

Anti-Patterns to Avoid

需避免的反模式

  • Do not use
    useEffect
    to fetch data - use queries instead
  • Do not store server state in local state (
    useState
    )
  • Do not forget to handle loading and error states
  • Do not create overly specific query keys that prevent cache reuse
  • Do not skip cache invalidation after mutations
  • Do not ignore the
    enabled
    option for conditional queries
  • 不要使用
    useEffect
    获取数据 - 改用查询钩子
  • 不要将服务端状态存储在本地状态(
    useState
    )中
  • 不要忘记处理加载和错误状态
  • 不要创建过于具体的查询键,以免影响缓存复用
  • 不要忽略修改后的缓存失效操作
  • 不要忽略条件查询的
    enabled
    选项