react-query-patterns

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

TanStack React Query Patterns

TanStack React Query 模式

Setup

配置

Query Client

查询客户端

tsx
// app/providers.tsx
'use client';

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { useState } from 'react';

export function Providers({ children }: { children: React.ReactNode }) {
  const [queryClient] = useState(
    () =>
      new QueryClient({
        defaultOptions: {
          queries: {
            staleTime: 60 * 1000, // 1 minute
            gcTime: 5 * 60 * 1000, // 5 minutes (formerly cacheTime)
            retry: 1,
            refetchOnWindowFocus: false,
          },
        },
      })
  );

  return (
    <QueryClientProvider client={queryClient}>
      {children}
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  );
}
tsx
// app/providers.tsx
'use client';

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { useState } from 'react';

export function Providers({ children }: { children: React.ReactNode }) {
  const [queryClient] = useState(
    () =>
      new QueryClient({
        defaultOptions: {
          queries: {
            staleTime: 60 * 1000, // 1分钟
            gcTime: 5 * 60 * 1000, // 5分钟(原cacheTime)
            retry: 1,
            refetchOnWindowFocus: false,
          },
        },
      })
  );

  return (
    <QueryClientProvider client={queryClient}>
      {children}
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  );
}

Queries

查询

Basic Query

基础查询

Fetch data with automatic caching:
tsx
'use client';

import { useQuery } from '@tanstack/react-query';

interface User {
  id: string;
  name: string;
  email: string;
}

export function UserProfile({ userId }: { userId: string }) {
  const { data, isLoading, error } = useQuery({
    queryKey: ['user', userId],
    queryFn: async () => {
      const response = await fetch(`/api/users/${userId}`);
      if (!response.ok) throw new Error('Failed to fetch user');
      return response.json() as Promise<User>;
    },
  });

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

  return <div>{data.name}</div>;
}
通过自动缓存获取数据:
tsx
'use client';

import { useQuery } from '@tanstack/react-query';

interface User {
  id: string;
  name: string;
  email: string;
}

export function UserProfile({ userId }: { userId: string }) {
  const { data, isLoading, error } = useQuery({
    queryKey: ['user', userId],
    queryFn: async () => {
      const response = await fetch(`/api/users/${userId}`);
      if (!response.ok) throw new Error('获取用户失败');
      return response.json() as Promise<User>;
    },
  });

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

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

Query with Options

带选项的查询

tsx
const { data, isLoading, error, refetch } = useQuery({
  queryKey: ['posts', { page, filter }],
  queryFn: () => fetchPosts(page, filter),
  enabled: !!userId, // Only run if userId exists
  staleTime: 5 * 60 * 1000, // Data fresh for 5 minutes
  gcTime: 10 * 60 * 1000, // Keep in cache for 10 minutes
  retry: 3, // Retry failed requests 3 times
  refetchInterval: 30 * 1000, // Refetch every 30 seconds
  refetchOnWindowFocus: true, // Refetch when window regains focus
});
tsx
const { data, isLoading, error, refetch } = useQuery({
  queryKey: ['posts', { page, filter }],
  queryFn: () => fetchPosts(page, filter),
  enabled: !!userId, // 仅当userId存在时执行
  staleTime: 5 * 60 * 1000, // 数据5分钟内保持新鲜
  gcTime: 10 * 60 * 1000, // 在缓存中保留10分钟
  retry: 3, // 失败请求重试3次
  refetchInterval: 30 * 1000, // 每30秒重新获取一次
  refetchOnWindowFocus: true, // 当窗口重新获得焦点时重新获取
});

Query Keys

查询键

Structure

结构

Hierarchical query key organization:
PatternExample
Simple
['users']
With ID
['users', userId]
With params
['posts', { page, filter }]
Nested
['users', userId, 'posts']
分层查询键组织:
模式示例
简单
['users']
带ID
['users', userId]
带参数
['posts', { page, filter }]
嵌套
['users', userId, 'posts']

Best Practices

最佳实践

  • Use arrays for all query keys
  • Order from general to specific
  • Include all variables that affect the query
  • Use objects for multiple parameters
  • Keep keys consistent across the app
  • 所有查询键都使用数组
  • 顺序从通用到具体
  • 包含所有影响查询的变量
  • 多个参数使用对象
  • 在整个应用中保持键的一致性

Query Key Factory

查询键工厂

tsx
// Query key organization
const queryKeys = {
  users: ['users'] as const,
  user: (id: string) => ['users', id] as const,
  userPosts: (id: string) => ['users', id, 'posts'] as const,
  posts: {
    all: ['posts'] as const,
    lists: () => ['posts', 'list'] as const,
    list: (filters: PostFilters) => ['posts', 'list', filters] as const,
    detail: (id: string) => ['posts', id] as const,
  },
};

// Usage
const { data } = useQuery({
  queryKey: queryKeys.user(userId),
  queryFn: () => fetchUser(userId),
});
tsx
// 查询键组织
const queryKeys = {
  users: ['users'] as const,
  user: (id: string) => ['users', id] as const,
  userPosts: (id: string) => ['users', id, 'posts'] as const,
  posts: {
    all: ['posts'] as const,
    lists: () => ['posts', 'list'] as const,
    list: (filters: PostFilters) => ['posts', 'list', filters] as const,
    detail: (id: string) => ['posts', id] as const,
  },
};

// 使用示例
const { data } = useQuery({
  queryKey: queryKeys.user(userId),
  queryFn: () => fetchUser(userId),
});

Mutations

数据突变(Mutations)

Basic Mutation

基础突变

Modify server data:
tsx
import { useMutation, useQueryClient } from '@tanstack/react-query';

export function CreatePostForm() {
  const queryClient = useQueryClient();

  const mutation = useMutation({
    mutationFn: async (newPost: NewPost) => {
      const response = await fetch('/api/posts', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(newPost),
      });
      if (!response.ok) throw new Error('Failed to create post');
      return response.json();
    },
    onSuccess: () => {
      // Invalidate and refetch posts query
      queryClient.invalidateQueries({ queryKey: ['posts'] });
    },
  });

  const handleSubmit = (data: NewPost) => {
    mutation.mutate(data);
  };

  return (
    <form onSubmit={handleSubmit}>
      {/* form fields */}
      {mutation.isPending && <p>Creating post...</p>}
      {mutation.isError && <p>Error: {mutation.error.message}</p>}
      {mutation.isSuccess && <p>Post created!</p>}
    </form>
  );
}
修改服务端数据:
tsx
import { useMutation, useQueryClient } from '@tanstack/react-query';

export function CreatePostForm() {
  const queryClient = useQueryClient();

  const mutation = useMutation({
    mutationFn: async (newPost: NewPost) => {
      const response = await fetch('/api/posts', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(newPost),
      });
      if (!response.ok) throw new Error('创建帖子失败');
      return response.json();
    },
    onSuccess: () => {
      // 使帖子查询失效并重新获取
      queryClient.invalidateQueries({ queryKey: ['posts'] });
    },
  });

  const handleSubmit = (data: NewPost) => {
    mutation.mutate(data);
  };

  return (
    <form onSubmit={handleSubmit}>
      {/* 表单字段 */}
      {mutation.isPending && <p>正在创建帖子...</p>}
      {mutation.isError && <p>错误:{mutation.error.message}</p>}
      {mutation.isSuccess && <p>帖子创建成功!</p>}
    </form>
  );
}

Optimistic Updates

乐观更新

Update UI immediately before server responds:
tsx
const mutation = useMutation({
  mutationFn: updateTodo,
  onMutate: async (newTodo) => {
    // Cancel outgoing refetches
    await queryClient.cancelQueries({ queryKey: ['todos'] });

    // Snapshot current value
    const previousTodos = queryClient.getQueryData(['todos']);

    // Optimistically update
    queryClient.setQueryData(['todos'], (old: Todo[]) => {
      return old.map((todo) =>
        todo.id === newTodo.id ? newTodo : todo
      );
    });

    // Return context with snapshot
    return { previousTodos };
  },
  onError: (err, newTodo, context) => {
    // Rollback on error
    queryClient.setQueryData(['todos'], context.previousTodos);
  },
  onSettled: () => {
    // Refetch after error or success
    queryClient.invalidateQueries({ queryKey: ['todos'] });
  },
});
在服务端响应前立即更新UI:
tsx
const mutation = useMutation({
  mutationFn: updateTodo,
  onMutate: async (newTodo) => {
    // 取消正在进行的重新获取
    await queryClient.cancelQueries({ queryKey: ['todos'] });

    // 快照当前值
    const previousTodos = queryClient.getQueryData(['todos']);

    // 乐观更新
    queryClient.setQueryData(['todos'], (old: Todo[]) => {
      return old.map((todo) =>
        todo.id === newTodo.id ? newTodo : todo
      );
    });

    // 返回包含快照的上下文
    return { previousTodos };
  },
  onError: (err, newTodo, context) => {
    // 出错时回滚
    queryClient.setQueryData(['todos'], context.previousTodos);
  },
  onSettled: () => {
    // 出错或成功后重新获取
    queryClient.invalidateQueries({ queryKey: ['todos'] });
  },
});

Mutation with Invalidation

带失效的突变

tsx
const mutation = useMutation({
  mutationFn: deletePost,
  onSuccess: (_, deletedPostId) => {
    // Invalidate list query
    queryClient.invalidateQueries({ queryKey: ['posts', 'list'] });

    // Remove deleted post from cache
    queryClient.removeQueries({ queryKey: ['posts', deletedPostId] });

    // Or update cache manually
    queryClient.setQueryData(['posts', 'list'], (old: Post[]) => {
      return old.filter((post) => post.id !== deletedPostId);
    });
  },
});
tsx
const mutation = useMutation({
  mutationFn: deletePost,
  onSuccess: (_, deletedPostId) => {
    // 使列表查询失效
    queryClient.invalidateQueries({ queryKey: ['posts', 'list'] });

    // 从缓存中移除已删除的帖子
    queryClient.removeQueries({ queryKey: ['posts', deletedPostId] });

    // 或手动更新缓存
    queryClient.setQueryData(['posts', 'list'], (old: Post[]) => {
      return old.filter((post) => post.id !== deletedPostId);
    });
  },
});

Infinite Queries

无限查询

Load more data as user scrolls:
tsx
import { useInfiniteQuery } from '@tanstack/react-query';

export function InfinitePostList() {
  const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
    isLoading,
  } = useInfiniteQuery({
    queryKey: ['posts', 'infinite'],
    queryFn: async ({ pageParam }) => {
      const response = await fetch(`/api/posts?page=${pageParam}`);
      return response.json();
    },
    initialPageParam: 1,
    getNextPageParam: (lastPage, allPages) => {
      return lastPage.hasMore ? allPages.length + 1 : undefined;
    },
  });

  if (isLoading) return <LoadingSkeleton />;

  return (
    <div>
      {data.pages.map((page, i) => (
        <div key={i}>
          {page.posts.map((post) => (
            <PostCard key={post.id} post={post} />
          ))}
        </div>
      ))}
      {hasNextPage && (
        <button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>
          {isFetchingNextPage ? 'Loading...' : 'Load More'}
        </button>
      )}
    </div>
  );
}
用户滚动时加载更多数据:
tsx
import { useInfiniteQuery } from '@tanstack/react-query';

export function InfinitePostList() {
  const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
    isLoading,
  } = useInfiniteQuery({
    queryKey: ['posts', 'infinite'],
    queryFn: async ({ pageParam }) => {
      const response = await fetch(`/api/posts?page=${pageParam}`);
      return response.json();
    },
    initialPageParam: 1,
    getNextPageParam: (lastPage, allPages) => {
      return lastPage.hasMore ? allPages.length + 1 : undefined;
    },
  });

  if (isLoading) return <LoadingSkeleton />;

  return (
    <div>
      {data.pages.map((page, i) => (
        <div key={i}>
          {page.posts.map((post) => (
            <PostCard key={post.id} post={post} />
          ))}
        </div>
      ))}
      {hasNextPage && (
        <button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>
          {isFetchingNextPage ? '加载中...' : '加载更多'}
        </button>
      )}
    </div>
  );
}

Prefetching

预获取

Hover Prefetch

悬停预获取

Prefetch data on hover for instant navigation:
tsx
import { useQueryClient } from '@tanstack/react-query';

export function PostLink({ postId }: { postId: string }) {
  const queryClient = useQueryClient();

  const prefetchPost = () => {
    queryClient.prefetchQuery({
      queryKey: ['posts', postId],
      queryFn: () => fetchPost(postId),
      staleTime: 60 * 1000, // Keep for 1 minute
    });
  };

  return (
    <Link
      href={`/posts/${postId}`}
      onMouseEnter={prefetchPost}
      onTouchStart={prefetchPost}
    >
      View Post
    </Link>
  );
}
在悬停时预获取数据以实现即时导航:
tsx
import { useQueryClient } from '@tanstack/react-query';

export function PostLink({ postId }: { postId: string }) {
  const queryClient = useQueryClient();

  const prefetchPost = () => {
    queryClient.prefetchQuery({
      queryKey: ['posts', postId],
      queryFn: () => fetchPost(postId),
      staleTime: 60 * 1000, // 保留1分钟
    });
  };

  return (
    <Link
      href={`/posts/${postId}`}
      onMouseEnter={prefetchPost}
      onTouchStart={prefetchPost}
    >
      查看帖子
    </Link>
  );
}

Page Prefetch

页面预获取

tsx
const { data } = useQuery({
  queryKey: ['posts', page],
  queryFn: () => fetchPosts(page),
});

// Prefetch next page
useEffect(() => {
  if (data?.hasMore) {
    queryClient.prefetchQuery({
      queryKey: ['posts', page + 1],
      queryFn: () => fetchPosts(page + 1),
    });
  }
}, [data, page, queryClient]);
tsx
const { data } = useQuery({
  queryKey: ['posts', page],
  queryFn: () => fetchPosts(page),
});

// 预获取下一页
useEffect(() => {
  if (data?.hasMore) {
    queryClient.prefetchQuery({
      queryKey: ['posts', page + 1],
      queryFn: () => fetchPosts(page + 1),
    });
  }
}, [data, page, queryClient]);

Dependent Queries

依赖查询

Query depends on result of previous query:
tsx
// First query
const { data: user } = useQuery({
  queryKey: ['user', userId],
  queryFn: () => fetchUser(userId),
});

// Second query depends on first
const { data: projects } = useQuery({
  queryKey: ['projects', user?.id],
  queryFn: () => fetchUserProjects(user.id),
  enabled: !!user, // Only run when user exists
});
查询依赖于前一个查询的结果:
tsx
// 第一个查询
const { data: user } = useQuery({
  queryKey: ['user', userId],
  queryFn: () => fetchUser(userId),
});

// 第二个查询依赖于第一个查询
const { data: projects } = useQuery({
  queryKey: ['projects', user?.id],
  queryFn: () => fetchUserProjects(user.id),
  enabled: !!user, // 仅当user存在时执行
});

Parallel Queries

并行查询

Multiple Independent Queries

多个独立查询

tsx
export function Dashboard() {
  const users = useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers,
  });

  const posts = useQuery({
    queryKey: ['posts'],
    queryFn: fetchPosts,
  });

  const stats = useQuery({
    queryKey: ['stats'],
    queryFn: fetchStats,
  });

  if (users.isLoading || posts.isLoading || stats.isLoading) {
    return <LoadingSkeleton />;
  }

  return (
    <div>
      <UserList users={users.data} />
      <PostList posts={posts.data} />
      <Stats data={stats.data} />
    </div>
  );
}
tsx
export function Dashboard() {
  const users = useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers,
  });

  const posts = useQuery({
    queryKey: ['posts'],
    queryFn: fetchPosts,
  });

  const stats = useQuery({
    queryKey: ['stats'],
    queryFn: fetchStats,
  });

  if (users.isLoading || posts.isLoading || stats.isLoading) {
    return <LoadingSkeleton />;
  }

  return (
    <div>
      <UserList users={users.data} />
      <PostList posts={posts.data} />
      <Stats data={stats.data} />
    </div>
  );
}

useQueries for Dynamic Queries

使用useQueries处理动态查询

tsx
import { useQueries } from '@tanstack/react-query';

export function MultiUserView({ userIds }: { userIds: string[] }) {
  const userQueries = useQueries({
    queries: userIds.map((id) => ({
      queryKey: ['user', id],
      queryFn: () => fetchUser(id),
    })),
  });

  const isLoading = userQueries.some((query) => query.isLoading);
  const users = userQueries.map((query) => query.data);

  if (isLoading) return <LoadingSkeleton />;

  return (
    <div>
      {users.map((user) => (
        <UserCard key={user.id} user={user} />
      ))}
    </div>
  );
}
tsx
import { useQueries } from '@tanstack/react-query';

export function MultiUserView({ userIds }: { userIds: string[] }) {
  const userQueries = useQueries({
    queries: userIds.map((id) => ({
      queryKey: ['user', id],
      queryFn: () => fetchUser(id),
    })),
  });

  const isLoading = userQueries.some((query) => query.isLoading);
  const users = userQueries.map((query) => query.data);

  if (isLoading) return <LoadingSkeleton />;

  return (
    <div>
      {users.map((user) => (
        <UserCard key={user.id} user={user} />
      ))}
    </div>
  );
}

Error Handling

错误处理

Retry Logic

重试逻辑

tsx
const { data, error } = useQuery({
  queryKey: ['posts'],
  queryFn: fetchPosts,
  retry: (failureCount, error) => {
    // Don't retry on 404
    if (error.status === 404) return false;
    // Retry up to 3 times
    return failureCount < 3;
  },
  retryDelay: (attemptIndex) => {
    // Exponential backoff: 1s, 2s, 4s
    return Math.min(1000 * 2 ** attemptIndex, 30000);
  },
});
tsx
const { data, error } = useQuery({
  queryKey: ['posts'],
  queryFn: fetchPosts,
  retry: (failureCount, error) => {
    // 404错误不重试
    if (error.status === 404) return false;
    // 最多重试3次
    return failureCount < 3;
  },
  retryDelay: (attemptIndex) => {
    // 指数退避:1秒、2秒、4秒
    return Math.min(1000 * 2 ** attemptIndex, 30000);
  },
});

Error Boundaries

错误边界

tsx
import { useQuery } from '@tanstack/react-query';
import { useErrorBoundary } from 'react-error-boundary';

export function CriticalData() {
  const { showBoundary } = useErrorBoundary();

  const { data } = useQuery({
    queryKey: ['critical'],
    queryFn: fetchCriticalData,
    throwOnError: true, // Throw errors to Error Boundary
  });

  return <div>{/* render data */}</div>;
}
tsx
import { useQuery } from '@tanstack/react-query';
import { useErrorBoundary } from 'react-error-boundary';

export function CriticalData() {
  const { showBoundary } = useErrorBoundary();

  const { data } = useQuery({
    queryKey: ['critical'],
    queryFn: fetchCriticalData,
    throwOnError: true, // 将错误抛出到Error Boundary
  });

  return <div>{/* 渲染数据 */}</div>;
}

Best Practices

最佳实践

Do

建议

  • Use query keys as array of dependencies
  • Invalidate queries after mutations
  • Prefetch on hover for better UX
  • Use staleTime to reduce unnecessary refetches
  • Implement optimistic updates for instant feedback
  • Use enabled option for dependent queries
  • Keep query functions pure and reusable
  • 将查询键用作依赖项数组
  • 突变后使查询失效
  • 悬停时预获取以提升用户体验
  • 使用staleTime减少不必要的重新获取
  • 实现乐观更新以获得即时反馈
  • 对依赖查询使用enabled选项
  • 保持查询函数纯净且可复用

Don't

不建议

  • Don't use query keys as strings (use arrays)
  • Don't mutate query data directly
  • Don't forget to handle loading and error states
  • Don't set staleTime too low (causes excessive requests)
  • Don't invalidate all queries (be specific)
  • Don't put business logic in queryFn (use services)
  • 不要将查询键用作字符串(使用数组)
  • 不要直接修改查询数据
  • 不要忘记处理加载和错误状态
  • 不要将staleTime设置得过低(会导致过多请求)
  • 不要使所有查询失效(要具体)
  • 不要在queryFn中放入业务逻辑(使用服务层)

Performance

性能

Optimization

优化

  • Set appropriate staleTime (avoid unnecessary refetches)
  • Use gcTime to control cache memory usage
  • Implement pagination or infinite queries for large lists
  • Prefetch predictable user navigation
  • Use select option to subscribe to only needed data
  • 设置合适的staleTime(避免不必要的重新获取)
  • 使用gcTime控制缓存内存使用
  • 对大型列表实现分页或无限查询
  • 预获取可预测的用户导航
  • 使用select选项仅订阅所需数据

Monitoring

监控

  • Enable React Query Devtools in development
  • Monitor network requests in DevTools
  • Check query cache size regularly
  • Measure query execution time
  • 在开发环境中启用React Query Devtools
  • 在开发者工具中监控网络请求
  • 定期检查查询缓存大小
  • 测量查询执行时间