tanstack-query-advanced

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

TanStack Query Advanced

TanStack Query 高级用法

Production patterns for TanStack Query v5 - server state management done right.
TanStack Query v5 生产级实践——正确管理服务端状态。

Overview

概述

  • Infinite scroll / pagination
  • Optimistic UI updates
  • Prefetching for instant navigation
  • Complex cache invalidation
  • Dependent/parallel queries
  • Mutations with rollback
  • 无限滚动/分页
  • 乐观UI更新
  • 为即时导航预取数据
  • 复杂缓存失效
  • 依赖/并行查询
  • 支持回滚的变更操作

Core Patterns

核心实践

1. Infinite Queries (Cursor-Based)

1. 基于游标无限查询

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

interface Page {
  items: Item[];
  nextCursor: string | null;
}

function useInfiniteItems() {
  return useInfiniteQuery({
    queryKey: ['items'],
    queryFn: async ({ pageParam }): Promise<Page> => {
      const res = await fetch(`/api/items?cursor=${pageParam ?? ''}`);
      return res.json();
    },
    initialPageParam: null as string | null,
    getNextPageParam: (lastPage) => lastPage.nextCursor,
    getPreviousPageParam: (firstPage) => firstPage.prevCursor,
  });
}

// Component
function ItemList() {
  const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteItems();

  return (
    <>
      {data?.pages.flatMap((page) => page.items.map((item) => (
        <ItemCard key={item.id} item={item} />
      )))}
      <button
        onClick={() => fetchNextPage()}
        disabled={!hasNextPage || isFetchingNextPage}
      >
        {isFetchingNextPage ? 'Loading...' : hasNextPage ? 'Load More' : 'No more'}
      </button>
    </>
  );
}
typescript
import { useInfiniteQuery } from '@tanstack/react-query';

interface Page {
  items: Item[];
  nextCursor: string | null;
}

function useInfiniteItems() {
  return useInfiniteQuery({
    queryKey: ['items'],
    queryFn: async ({ pageParam }): Promise<Page> => {
      const res = await fetch(`/api/items?cursor=${pageParam ?? ''}`);
      return res.json();
    },
    initialPageParam: null as string | null,
    getNextPageParam: (lastPage) => lastPage.nextCursor,
    getPreviousPageParam: (firstPage) => firstPage.prevCursor,
  });
}

// Component
function ItemList() {
  const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteItems();

  return (
    <>
      {data?.pages.flatMap((page) => page.items.map((item) => (
        <ItemCard key={item.id} item={item} />
      )))}
      <button
        onClick={() => fetchNextPage()}
        disabled={!hasNextPage || isFetchingNextPage}
      >
        {isFetchingNextPage ? 'Loading...' : hasNextPage ? 'Load More' : 'No more'}
      </button>
    </>
  );
}

2. Optimistic Updates

2. 乐观更新

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

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

  return useMutation({
    mutationFn: updateTodo,
    onMutate: async (newTodo) => {
      // Cancel outgoing refetches
      await queryClient.cancelQueries({ queryKey: ['todos', newTodo.id] });

      // Snapshot previous value
      const previousTodo = queryClient.getQueryData(['todos', newTodo.id]);

      // Optimistically update
      queryClient.setQueryData(['todos', newTodo.id], newTodo);

      // Return context for rollback
      return { previousTodo };
    },
    onError: (err, newTodo, context) => {
      // Rollback on error
      queryClient.setQueryData(['todos', newTodo.id], context?.previousTodo);
    },
    onSettled: (data, error, variables) => {
      // Always refetch after error or success
      queryClient.invalidateQueries({ queryKey: ['todos', variables.id] });
    },
  });
}
typescript
import { useMutation, useQueryClient } from '@tanstack/react-query';

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

  return useMutation({
    mutationFn: updateTodo,
    onMutate: async (newTodo) => {
      // Cancel outgoing refetches
      await queryClient.cancelQueries({ queryKey: ['todos', newTodo.id] });

      // Snapshot previous value
      const previousTodo = queryClient.getQueryData(['todos', newTodo.id]);

      // Optimistically update
      queryClient.setQueryData(['todos', newTodo.id], newTodo);

      // Return context for rollback
      return { previousTodo };
    },
    onError: (err, newTodo, context) => {
      // Rollback on error
      queryClient.setQueryData(['todos', newTodo.id], context?.previousTodo);
    },
    onSettled: (data, error, variables) => {
      // Always refetch after error or success
      queryClient.invalidateQueries({ queryKey: ['todos', variables.id] });
    },
  });
}

3. Prefetching Patterns

3. 预取实践

typescript
// Prefetch on hover
function UserLink({ userId }: { userId: string }) {
  const queryClient = useQueryClient();

  const prefetchUser = () => {
    queryClient.prefetchQuery({
      queryKey: ['user', userId],
      queryFn: () => fetchUser(userId),
      staleTime: 5 * 60 * 1000, // 5 minutes
    });
  };

  return (
    <Link to={`/users/${userId}`} onMouseEnter={prefetchUser}>
      View User
    </Link>
  );
}

// Prefetch in loader (React Router)
export const loader = (queryClient: QueryClient) => async ({ params }) => {
  await queryClient.ensureQueryData({
    queryKey: ['user', params.id],
    queryFn: () => fetchUser(params.id),
  });
  return null;
};
typescript
// Prefetch on hover
function UserLink({ userId }: { userId: string }) {
  const queryClient = useQueryClient();

  const prefetchUser = () => {
    queryClient.prefetchQuery({
      queryKey: ['user', userId],
      queryFn: () => fetchUser(userId),
      staleTime: 5 * 60 * 1000, // 5 minutes
    });
  };

  return (
    <Link to={`/users/${userId}`} onMouseEnter={prefetchUser}>
      View User
    </Link>
  );
}

// Prefetch in loader (React Router)
export const loader = (queryClient: QueryClient) => async ({ params }) => {
  await queryClient.ensureQueryData({
    queryKey: ['user', params.id],
    queryFn: () => fetchUser(params.id),
  });
  return null;
};

4. Smart Cache Invalidation

4. 智能缓存失效

typescript
const queryClient = useQueryClient();

// Invalidate exact query
queryClient.invalidateQueries({ queryKey: ['todos', 1] });

// Invalidate all todos queries
queryClient.invalidateQueries({ queryKey: ['todos'] });

// Invalidate with predicate
queryClient.invalidateQueries({
  predicate: (query) =>
    query.queryKey[0] === 'todos' &&
    (query.queryKey[1] as Todo)?.status === 'done',
});

// Invalidate and refetch immediately
queryClient.refetchQueries({ queryKey: ['todos'], type: 'active' });

// Remove from cache entirely
queryClient.removeQueries({ queryKey: ['todos', 1] });
typescript
const queryClient = useQueryClient();

// Invalidate exact query
queryClient.invalidateQueries({ queryKey: ['todos', 1] });

// Invalidate all todos queries
queryClient.invalidateQueries({ queryKey: ['todos'] });

// Invalidate with predicate
queryClient.invalidateQueries({
  predicate: (query) =>
    query.queryKey[0] === 'todos' &&
    (query.queryKey[1] as Todo)?.status === 'done',
});

// Invalidate and refetch immediately
queryClient.refetchQueries({ queryKey: ['todos'], type: 'active' });

// Remove from cache entirely
queryClient.removeQueries({ queryKey: ['todos', 1] });

5. Dependent Queries

5. 依赖查询

typescript
function useUserPosts(userId: string) {
  // First query
  const userQuery = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
  });

  // Dependent query - only runs when user is loaded
  const postsQuery = useQuery({
    queryKey: ['posts', userId],
    queryFn: () => fetchUserPosts(userId),
    enabled: !!userQuery.data, // Only fetch when user exists
  });

  return { user: userQuery.data, posts: postsQuery.data };
}
typescript
function useUserPosts(userId: string) {
  // First query
  const userQuery = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
  });

  // Dependent query - only runs when user is loaded
  const postsQuery = useQuery({
    queryKey: ['posts', userId],
    queryFn: () => fetchUserPosts(userId),
    enabled: !!userQuery.data, // Only fetch when user exists
  });

  return { user: userQuery.data, posts: postsQuery.data };
}

6. Parallel Queries

6. 并行查询

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

function useMultipleUsers(userIds: string[]) {
  return useQueries({
    queries: userIds.map((id) => ({
      queryKey: ['user', id],
      queryFn: () => fetchUser(id),
      staleTime: 5 * 60 * 1000,
    })),
    combine: (results) => ({
      users: results.map((r) => r.data).filter(Boolean),
      pending: results.some((r) => r.isPending),
      error: results.find((r) => r.error)?.error,
    }),
  });
}
typescript
import { useQueries } from '@tanstack/react-query';

function useMultipleUsers(userIds: string[]) {
  return useQueries({
    queries: userIds.map((id) => ({
      queryKey: ['user', id],
      queryFn: () => fetchUser(id),
      staleTime: 5 * 60 * 1000,
    })),
    combine: (results) => ({
      users: results.map((r) => r.data).filter(Boolean),
      pending: results.some((r) => r.isPending),
      error: results.find((r) => r.error)?.error,
    }),
  });
}

7. Query Deduplication & Batching

7. 查询去重与批处理

typescript
// Configure in QueryClient
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60, // 1 minute
      gcTime: 1000 * 60 * 5, // 5 minutes (formerly cacheTime)
      refetchOnWindowFocus: false,
      retry: 3,
      retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
    },
  },
});
typescript
// Configure in QueryClient
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60, // 1 minute
      gcTime: 1000 * 60 * 5, // 5 minutes (formerly cacheTime)
      refetchOnWindowFocus: false,
      retry: 3,
      retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
    },
  },
});

8. Suspense Integration

8. Suspense 集成

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

function UserProfile({ userId }: { userId: string }) {
  // This will suspend until data is ready
  const { data: user } = useSuspenseQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
  });

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

// Wrap with Suspense
<Suspense fallback={<Skeleton />}>
  <UserProfile userId="123" />
</Suspense>
typescript
import { useSuspenseQuery } from '@tanstack/react-query';

function UserProfile({ userId }: { userId: string }) {
  // This will suspend until data is ready
  const { data: user } = useSuspenseQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
  });

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

// Wrap with Suspense
<Suspense fallback={<Skeleton />}>
  <UserProfile userId="123" />
</Suspense>

9. Mutation State Tracking

9. 变更状态追踪

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

function PendingTodos() {
  // Track all pending todo mutations
  const pendingMutations = useMutationState({
    filters: { mutationKey: ['addTodo'], status: 'pending' },
    select: (mutation) => mutation.state.variables as Todo,
  });

  return (
    <>
      {pendingMutations.map((todo) => (
        <TodoItem key={todo.id} todo={todo} isPending />
      ))}
    </>
  );
}
typescript
import { useMutationState } from '@tanstack/react-query';

function PendingTodos() {
  // Track all pending todo mutations
  const pendingMutations = useMutationState({
    filters: { mutationKey: ['addTodo'], status: 'pending' },
    select: (mutation) => mutation.state.variables as Todo,
  });

  return (
    <>
      {pendingMutations.map((todo) => (
        <TodoItem key={todo.id} todo={todo} isPending />
      ))}
    </>
  );
}

Configuration Best Practices

配置最佳实践

typescript
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60,       // Data fresh for 1 min
      gcTime: 1000 * 60 * 5,      // Cache for 5 min
      refetchOnWindowFocus: true, // Refetch on tab focus
      refetchOnReconnect: true,   // Refetch on network reconnect
      retry: 3,                   // Retry failed requests
    },
    mutations: {
      retry: 1,
      onError: (error) => toast.error(error.message),
    },
  },
});
typescript
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60,       // Data fresh for 1 min
      gcTime: 1000 * 60 * 5,      // Cache for 5 min
      refetchOnWindowFocus: true, // Refetch on tab focus
      refetchOnReconnect: true,   // Refetch on network reconnect
      retry: 3,                   // Retry failed requests
    },
    mutations: {
      retry: 1,
      onError: (error) => toast.error(error.message),
    },
  },
});

Quick Reference

快速参考

typescript
// ✅ Create typed query with queryOptions helper (v5)
const userQueryOptions = (id: string) => queryOptions({
  queryKey: ['user', id] as const,
  queryFn: () => fetchUser(id),
  staleTime: 5 * 60 * 1000,
});

// ✅ Use the query options for consistency
const { data } = useQuery(userQueryOptions(userId));
await queryClient.prefetchQuery(userQueryOptions(userId));
await queryClient.ensureQueryData(userQueryOptions(userId));

// ✅ v5: isPending instead of isLoading (initial load only)
if (isPending) return <Skeleton />;

// ✅ v5: gcTime instead of cacheTime
gcTime: 5 * 60 * 1000,

// ✅ useSuspenseQuery for Suspense integration
const { data } = useSuspenseQuery(userQueryOptions(userId));

// ✅ Selective invalidation
queryClient.invalidateQueries({ queryKey: ['todos'], exact: true });

// ❌ NEVER destructure useQuery result at call site
const { data, isLoading } = useQuery({ queryKey: ['users'] }); // BAD - recreates object

// ❌ NEVER use string keys
useQuery({ queryKey: 'users' }); // BAD - use arrays

// ❌ NEVER store server state in Zustand
const useStore = create((set) => ({ users: [] })); // BAD - use React Query
typescript
// ✅ Create typed query with queryOptions helper (v5)
const userQueryOptions = (id: string) => queryOptions({
  queryKey: ['user', id] as const,
  queryFn: () => fetchUser(id),
  staleTime: 5 * 60 * 1000,
});

// ✅ Use the query options for consistency
const { data } = useQuery(userQueryOptions(userId));
await queryClient.prefetchQuery(userQueryOptions(userId));
await queryClient.ensureQueryData(userQueryOptions(userId));

// ✅ v5: isPending instead of isLoading (initial load only)
if (isPending) return <Skeleton />;

// ✅ v5: gcTime instead of cacheTime
gcTime: 5 * 60 * 1000,

// ✅ useSuspenseQuery for Suspense integration
const { data } = useSuspenseQuery(userQueryOptions(userId));

// ✅ Selective invalidation
queryClient.invalidateQueries({ queryKey: ['todos'], exact: true });

// ❌ NEVER destructure useQuery result at call site
const { data, isLoading } = useQuery({ queryKey: ['users'] }); // BAD - recreates object

// ❌ NEVER use string keys
useQuery({ queryKey: 'users' }); // BAD - use arrays

// ❌ NEVER store server state in Zustand
const useStore = create((set) => ({ users: [] })); // BAD - use React Query

Key Decisions

关键决策

DecisionOption AOption BRecommendation
Query key structureStringArrayArray - supports hierarchy and serialization
Cache timingstaleTimegcTimeBoth - staleTime for freshness, gcTime for memory
Loading stateisLoadingisPendingisPending (v5) - isLoading includes background refetches
Query definitionInlinequeryOptionsqueryOptions - reusable for prefetch/loader/useQuery
SuspenseuseQuery + loadinguseSuspenseQueryuseSuspenseQuery for React 18+ Suspense
Optimistic updatessetQueryData onlysetQueryData + invalidateBoth - optimistic then reconcile
Parallel queriesMultiple useQueryuseQueriesuseQueries - combined loading/error state
Infinite queriesManual paginationuseInfiniteQueryuseInfiniteQuery - built-in cursor handling
决策项选项A选项B推荐方案
查询键结构字符串数组数组 - 支持层级与序列化
缓存时机staleTimegcTime两者结合 - staleTime控制新鲜度,gcTime控制内存缓存时长
加载状态isLoadingisPendingisPending(v5)- isLoading包含后台重新获取状态
查询定义内联queryOptionsqueryOptions - 可复用用于预取/加载器/useQuery
Suspense集成useQuery + 加载状态useSuspenseQueryuseSuspenseQuery - 适用于React 18+ Suspense
乐观更新仅setQueryDatasetQueryData + 失效两者结合 - 先乐观更新再同步服务端
并行查询多个useQueryuseQueriesuseQueries - 合并加载/错误状态
无限查询手动分页useInfiniteQueryuseInfiniteQuery - 内置游标处理

Anti-Patterns (FORBIDDEN)

反模式(禁止)

typescript
// ❌ FORBIDDEN: Storing server state in Zustand/Redux
const useStore = create((set) => ({
  users: [],  // Server state belongs in React Query!
  fetchUsers: async () => {
    const users = await api.getUsers();
    set({ users }); // Stale data, no background refetch
  },
}));

// ❌ FORBIDDEN: String query keys
useQuery({
  queryKey: 'todos',  // Must be an array!
  queryFn: fetchTodos,
});

// ❌ FORBIDDEN: Using deprecated cacheTime (v5)
useQuery({
  queryKey: ['todos'],
  cacheTime: 5 * 60 * 1000,  // WRONG - use gcTime in v5
});

// ❌ FORBIDDEN: Using isLoading for initial state (v5)
// isLoading = isPending && isFetching (includes background refetch)
if (isLoading) return <Skeleton />;  // WRONG - use isPending

// ❌ FORBIDDEN: Over-invalidating after mutations
useMutation({
  mutationFn: updateTodo,
  onSuccess: () => {
    queryClient.invalidateQueries(); // Invalidates EVERYTHING!
  },
});

// ❌ FORBIDDEN: Forgetting to cancel queries in optimistic updates
useMutation({
  onMutate: async (newTodo) => {
    // Missing: await queryClient.cancelQueries(...)
    const previous = queryClient.getQueryData(['todos']);
    queryClient.setQueryData(['todos'], (old) => [...old, newTodo]);
    return { previous };
  },
});

// ❌ FORBIDDEN: Not returning context from onMutate
useMutation({
  onMutate: async (newTodo) => {
    const previous = queryClient.getQueryData(['todos']);
    queryClient.setQueryData(['todos'], (old) => [...old, newTodo]);
    // Missing: return { previous }; // Required for rollback!
  },
  onError: (err, newTodo, context) => {
    queryClient.setQueryData(['todos'], context?.previous); // context is undefined!
  },
});

// ❌ FORBIDDEN: Mutating cache data directly
queryClient.setQueryData(['todos'], (old) => {
  old.push(newTodo);  // WRONG - mutates existing array
  return old;
});
// ✅ CORRECT: Return new array
queryClient.setQueryData(['todos'], (old) => [...old, newTodo]);

// ❌ FORBIDDEN: Fetching inside useEffect
useEffect(() => {
  fetch('/api/users').then(setUsers);  // Use React Query instead!
}, []);
typescript
// ❌ FORBIDDEN: Storing server state in Zustand/Redux
const useStore = create((set) => ({
  users: [],  // Server state belongs in React Query!
  fetchUsers: async () => {
    const users = await api.getUsers();
    set({ users }); // Stale data, no background refetch
  },
}));

// ❌ FORBIDDEN: String query keys
useQuery({
  queryKey: 'todos',  // Must be an array!
  queryFn: fetchTodos,
});

// ❌ FORBIDDEN: Using deprecated cacheTime (v5)
useQuery({
  queryKey: ['todos'],
  cacheTime: 5 * 60 * 1000,  // WRONG - use gcTime in v5
});

// ❌ FORBIDDEN: Using isLoading for initial state (v5)
// isLoading = isPending && isFetching (includes background refetch)
if (isLoading) return <Skeleton />;  // WRONG - use isPending

// ❌ FORBIDDEN: Over-invalidating after mutations
useMutation({
  mutationFn: updateTodo,
  onSuccess: () => {
    queryClient.invalidateQueries(); // Invalidates EVERYTHING!
  },
});

// ❌ FORBIDDEN: Forgetting to cancel queries in optimistic updates
useMutation({
  onMutate: async (newTodo) => {
    // Missing: await queryClient.cancelQueries(...)
    const previous = queryClient.getQueryData(['todos']);
    queryClient.setQueryData(['todos'], (old) => [...old, newTodo]);
    return { previous };
  },
});

// ❌ FORBIDDEN: Not returning context from onMutate
useMutation({
  onMutate: async (newTodo) => {
    const previous = queryClient.getQueryData(['todos']);
    queryClient.setQueryData(['todos'], (old) => [...old, newTodo]);
    // Missing: return { previous }; // Required for rollback!
  },
  onError: (err, newTodo, context) => {
    queryClient.setQueryData(['todos'], context?.previous); // context is undefined!
  },
});

// ❌ FORBIDDEN: Mutating cache data directly
queryClient.setQueryData(['todos'], (old) => {
  old.push(newTodo);  // WRONG - mutates existing array
  return old;
});
// ✅ CORRECT: Return new array
queryClient.setQueryData(['todos'], (old) => [...old, newTodo]);

// ❌ FORBIDDEN: Fetching inside useEffect
useEffect(() => {
  fetch('/api/users').then(setUsers);  // Use React Query instead!
}, []);

Related Skills

相关技能

  • zustand-patterns
    - Client state management (use alongside React Query for server state)
  • form-state-patterns
    - Form state with React Hook Form (integrate mutation status)
  • msw-mocking
    - Mock Service Worker for testing queries without network
  • react-server-components-framework
    - RSC hydration with React Query
  • zustand-patterns
    - 客户端状态管理(与React Query配合用于服务端状态)
  • form-state-patterns
    - 结合React Hook Form的表单状态(集成变更状态)
  • msw-mocking
    - Mock Service Worker,无需网络即可测试查询
  • react-server-components-framework
    - React Query与RSC水合

Capability Details

功能细节

infinite-queries

infinite-queries

Keywords: infinite, pagination, cursor, load more, scroll, pages Solves: Implementing cursor-based pagination with automatic page management
关键词: infinite, pagination, cursor, load more, scroll, pages 解决问题: 实现基于游标的分页,自动管理页面

optimistic-updates

optimistic-updates

Keywords: optimistic, instant, rollback, onMutate, setQueryData, cancel Solves: Showing immediate UI feedback before server confirmation with rollback
关键词: optimistic, instant, rollback, onMutate, setQueryData, cancel 解决问题: 在服务端确认前显示即时UI反馈,并支持回滚

prefetching

prefetching

Keywords: prefetch, hover, preload, ensureQueryData, loader, navigation Solves: Loading data before it's needed for instant navigation
关键词: prefetch, hover, preload, ensureQueryData, loader, navigation 解决问题: 在需要前加载数据,实现即时导航

cache-invalidation

cache-invalidation

Keywords: invalidate, refetch, stale, fresh, gcTime, staleTime, exact Solves: Keeping cache in sync with server after mutations
关键词: invalidate, refetch, stale, fresh, gcTime, staleTime, exact 解决问题: 变更后保持缓存与服务端同步

suspense-integration

suspense-integration

Keywords: suspense, useSuspenseQuery, streaming, fallback, boundary Solves: Integrating with React Suspense for declarative loading states
关键词: suspense, useSuspenseQuery, streaming, fallback, boundary 解决问题: 与React Suspense集成,实现声明式加载状态

parallel-queries

parallel-queries

Keywords: useQueries, parallel, concurrent, combine, batch Solves: Fetching multiple independent queries with combined state
关键词: useQueries, parallel, concurrent, combine, batch 解决问题: 获取多个独立查询,并合并状态

References

参考资料

  • references/cache-strategies.md
    - Cache invalidation patterns
  • scripts/query-hooks-template.ts
    - Production query hook template
  • checklists/tanstack-checklist.md
    - Implementation checklist
  • examples/tanstack-examples.md
    - Real-world usage examples
  • references/cache-strategies.md
    - 缓存失效模式
  • scripts/query-hooks-template.ts
    - 生产级查询Hook模板
  • checklists/tanstack-checklist.md
    - 实现检查清单
  • examples/tanstack-examples.md
    - 真实场景使用示例