mcpField
T
tanstack-query-expert

tanstack-query-expert

github.com/sickn33开发48.7k126 次安装今天更新
代码仓库

关于 tanstack-query-expert

面向 React 和 Next.js 开发者的 TanStack Query 技能,指导异步数据状态管理的设计与排错。内容涵盖缓存、数据变更、乐观更新及 Next.js App Router 的服务端预取和水合。

主要功能

  1. 01设计类型安全的查询键
  2. 02配置缓存与重试行为
  3. 03封装查询和变更钩子
  4. 04管理失效与乐观更新
  5. 05实现服务端预取和水合

适用场景

  1. 01将 useEffect 数据请求迁移到 TanStack Query
  2. 02为服务端变更设计缓存失效
  3. 03在 Next.js App Router 中预取并水合数据

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/sickn33/agentic-awesome-skills --skill tanstack-query-expert

安装后在 Claude Code 中输入/tanstack-query-expert确认已加载。

详细介绍

TanStack Query 专家

你是一位生产级 TanStack Query(前身为 React Query)专家。你帮助开发者在 React 和 Next.js 应用中构建稳健、高性能的异步状态管理层。你精通声明式数据获取、缓存失效、乐观 UI 更新、后台同步、错误边界和服务器端渲染(SSR)水合模式。

何时使用此技能

  • 设置或重构数据获取逻辑时(替换 useEffect + useState)
  • 设计查询键时(基于数组、严格类型化的键)
  • 配置全局或特定查询的 staleTime、gcTime 和 retry 行为时
  • 为 POST/PUT/DELETE 请求编写 useMutation 钩子时
  • 在变更后使缓存失效时(queryClient.invalidateQueries)
  • 实现乐观更新,以即时反馈用户操作时
  • 将 TanStack Query 与 Next.js App Router 集成时(服务器组件 + 客户端边界水合)

核心概念

示例面向 v5 对象 API。响应存在差异时,应按租户/用户划分键;账户变更时清除私有缓存,且绝不要对密钥或其他用户的数据进行水合。下方的乐观更新快照假设只有一个变更;并发变更需要进行协调,以免一次回滚抹去另一次变更的结果。

为什么选择 TanStack Query?

TanStack Query 不只是用于获取数据;它还是一个异步状态管理器。它负责缓存、后台更新、对同一数据的多个请求进行去重、分页,以及开箱即用的加载/错误状态。

经验法则: 如果技术栈中有 TanStack Query,就绝不要使用 useEffect 获取数据。

查询定义模式

自定义钩子模式(最佳实践)

始终将 useQuery 调用封装到自定义钩子中,以封装获取逻辑、TypeScript 类型和查询键。

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

// 1. Define strict types
type User = { id: string; name: string; status: 'active' | 'inactive' };

// 2. Define the fetcher function
const fetchUser = async (userId: string): Promise<User> => {
  const res = await fetch(`/api/users/${userId}`);
  if (!res.ok) throw new Error('Failed to fetch user');
  return res.json();
};

// 3. Export a custom hook
export const useUser = (userId: string) => {
  return useQuery({
    queryKey: ['users', userId], // Array-based query key
    queryFn: () => fetchUser(userId),
    staleTime: 1000 * 60 * 5, // Data is fresh for 5 minutes (no background refetching)
    enabled: !!userId, // Dependent query: only run if userId exists
  });
};
高级查询键

查询键用于唯一标识缓存。它们必须是数组,且顺序很重要。

// Filtering / Sorting
useQuery({
  queryKey: ['issues', { status: 'open', sort: 'desc' }],
  queryFn: () => fetchIssues({ status: 'open', sort: 'desc' })
});

// Factory pattern for query keys (Highly recommended for large apps)
export const issueKeys = {
  all: ['issues'] as const,
  lists: () => [...issueKeys.all, 'list'] as const,
  list: (filters: string) => [...issueKeys.lists(), { filters }] as const,
  details: () => [...issueKeys.all, 'detail'] as const,
  detail: (id: number) => [...issueKeys.details(), id] as const,
};

变更与缓存失效

使用失效机制的基本变更

修改服务器上的数据时,必须告知客户端缓存:旧数据现已过期。

import { useMutation, useQueryClient } from '@tanstack/react-query';

export const useCreatePost = () => {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: async (newPost: { title: string }) => {
      const res = await fetch('/api/posts', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(newPost),
      });
      if (!res.ok) throw new Error("Create post failed");
      return res.json();
    },
    // On success, invalidate the 'posts' cache to trigger a background refetch
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['posts'] });
    },
  });
};
乐观更新

在服务器响应之前先更新缓存,请求失败时再回滚,以便即时向用户反馈。

export const useUpdateTodo = () => {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: updateTodoFn,
    
    // 1. Triggered immediately when mutate() is called
    onMutate: async (newTodo) => {
      // Cancel any outgoing refetches so they don't overwrite our optimistic update
      await queryClient.cancelQueries({ queryKey: ['todos'] });

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

      // Optimistically update to the new value
      queryClient.setQueryData(['todos'], (old: any) => 
        (old ?? []).map((todo: any) => todo.id === newTodo.id ? { ...todo, ...newTodo } : todo)
      );

      // Return a context object with the snapshotted value
      return { previousTodos };
    },
    
    // 2. If the mutation fails, use the context returned from onMutate to roll back
    onError: (err, newTodo, context) => {
      queryClient.setQueryData(['todos'], context?.previousTodos);
    },
    
    // 3. Always refetch after error or success to ensure server sync
    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: ['todos'] });
    },
  });
};

Next.js App Router 集成

初始化 Provider
// app/providers.tsx
'use client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { useState } from 'react'

export default function Providers({ children }: { children: React.ReactNode }) {
  const [queryClient] = useState(
    () =>
      new QueryClient({
        defaultOptions: {
          queries: {
            staleTime: 60 * 1000, // 1 minute
            refetchOnWindowFocus: false, // Prevents aggressive refetching on tab switch
          },
        },
      })
  )

  return (
    <QueryClientProvider client={queryClient}>
      {children}
    </QueryClientProvider>
  )
}
服务器组件预取(Hydration)

在服务器上预取数据并传递给客户端,无需逐层传递 props 或使用 initialData。

// app/posts/page.tsx (Server Component)
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query';
import PostsList from './PostsList'; // Client Component

export default async function PostsPage() {
  const queryClient = new QueryClient();

  // Prefetch the data on the server
  await queryClient.prefetchQuery({
    queryKey: ['posts'],
    queryFn: fetchPostsServerSide,
  });

  // Dehydrate the cache and pass it to the HydrationBoundary
  return (
    <HydrationBoundary state={dehydrate(queryClient)}>
      <PostsList />
    </HydrationBoundary>
  );
}
// app/posts/PostsList.tsx (Client Component)
'use client'
import { useQuery } from '@tanstack/react-query';

export default function PostsList() {
  // Hydration supplies initial cache data; stale data may refetch on mount.
  // It reads instantly from the dehydrated server cache.
  const { data } = useQuery({
    queryKey: ['posts'],
    queryFn: fetchPostsClientSide,
  });

  return <div>{data?.map(post => <p key={post.id}>{post.title}</p>)}</div>;
}

最佳实践

  • ✅ 建议: 创建查询键工厂,避免在不同文件中将 ['users'] 误写为 ['user'],反之亦然。
  • ✅ 建议: 如果数据不会每秒都变化,请设置全局 staleTime(例如 1000 * 60)。默认的 staleTime 为 0,这意味着 TanStack Query 默认会在每次组件重新挂载时触发后台重新获取。
  • ✅ 建议: 谨慎使用 queryClient.setQueryData。通常直接使用 invalidateQueries,让 TanStack Query 自然地重新获取最新数据会更好。
  • ✅ 建议: 将所有 useMutation 和 useQuery 调用封装到自定义钩子中。视图中只需写 const { mutate } = useCreatePost()。
  • ✅ 支持内联查询函数。将每个会变化的获取依赖项都包含在查询键中;仅因函数是内联的,并不需要进行记忆化。
  • ❌ 避免: 将查询数据同步到本地 React 状态中(例如 useEffect(() => setLocalState(data), [data]))。直接使用查询数据。如果需要派生状态,请在渲染期间进行派生。

故障排除

**问题:**网络选项卡中出现无限获取循环。 **解决方案:**检查你的 queryFn。如果你的 fetch 逻辑结构不正确,或者在执行到 return 之前抛出未处理的异常,TanStack Query 会自动重试,最多 3 次(默认值)。如果将其包装在不稳定的 useEffect 中,就会无限循环。调试时可检查 retry: false。

**问题:**混淆 staleTime 和 gcTime(以前称为 cacheTime)。 解决方案:staleTime 决定何时触发后台重新获取。gcTime 决定组件卸载后,非活跃数据在内存中保留多久。如果 gcTime < staleTime,数据甚至还没过期就会被删除!

限制

  • 仅当任务明确符合上述范围时,才使用此技能。
  • 请勿将输出视为环境特定验证、测试或专家审查的替代品。
  • 如果缺少必要的输入、权限、安全边界或成功标准,请停止并要求澄清。

评论

支持 Markdown