mcpField
T
tanstack-query

tanstack-query

github.com/bobmatnyc开发78590 次安装15 周前更新
代码仓库

关于 tanstack-query

面向 React 开发者的 TanStack Query 实用指南,用于管理异步服务端状态。内容涵盖缓存、同步、变更、分页及相关查询模式。

主要功能

  1. 01缓存并同步服务端数据
  2. 02在后台重新获取过期数据
  3. 03编排依赖查询与并行查询
  4. 04处理变更和缓存失效
  5. 05实现乐观更新
  6. 06构建分页和无限查询

适用场景

  1. 01在 React 中获取并缓存 API 数据
  2. 02通过轮询刷新仪表板
  3. 03在变更响应前更新界面
  4. 04实现分页列表或无限滚动列表

安装

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

$npx skills add https://github.com/bobmatnyc/claude-mpm-skills --skill tanstack-query

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

附带文件metadata.json

详细介绍

TanStack Query(React Query)技能

概要

TanStack Query(原名 React Query)是一款功能强大的 React 异步状态管理库,可处理服务端状态的获取、缓存、同步和更新。它免去了手动获取数据所需的大量样板代码,并内置后台重新获取、乐观更新、分页和智能缓存管理等功能。

适用场景

在以下场景中使用 TanStack Query:

  • 从 REST API、GraphQL 或 tRPC 端点获取数据
  • 需要自动在后台重新获取数据并使缓存失效
  • 使用轮询或 WebSocket 数据构建实时仪表板
  • 实现无限滚动或分页
  • 需要为变更操作实现乐观 UI 更新
  • 管理复杂的服务端状态同步
  • 需要通过缓存持久化提供离线支持
  • 构建数据频繁更新的应用

TanStack Query 擅长处理:

  • 服务端状态管理(API 数据、外部状态)
  • 请求去重和缓存
  • stale-while-revalidate 模式
  • 加载和错误状态管理
  • 预取和提前加载
  • 并行查询和依赖查询编排

以下场景不建议使用 TanStack Query:

  • 纯客户端状态(使用 Zustand、Jotai 或 Context)
  • 表单状态管理(使用 React Hook Form、Formik)
  • 不需要缓存的简单一次性数据获取

快速入门

安装
npm install @tanstack/react-query
# DevTools (optional but recommended)
npm install @tanstack/react-query-devtools
基本设置
// 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
        refetchOnWindowFocus: false,
      },
    },
  }));

  return (
    <QueryClientProvider client={queryClient}>
      {children}
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  );
}
首次查询
// components/UserProfile.tsx
import { useQuery } from '@tanstack/react-query';

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

function UserProfile({ userId }: { userId: number }) {
  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 <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <div>
      <h1>{data.name}</h1>
      <p>{data.email}</p>
    </div>
  );
}
首次变更
// components/CreateUserForm.tsx
import { useMutation, useQueryClient } from '@tanstack/react-query';

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

  const mutation = useMutation({
    mutationFn: async (newUser: { name: string; email: string }) => {
      const response = await fetch('/api/users', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(newUser),
      });
      return response.json();
    },
    onSuccess: () => {
      // Invalidate and refetch users list
      queryClient.invalidateQueries({ queryKey: ['users'] });
    },
  });

  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const formData = new FormData(e.currentTarget);
    mutation.mutate({
      name: formData.get('name') as string,
      email: formData.get('email') as string,
    });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input name="name" placeholder="Name" required />
      <input name="email" type="email" placeholder="Email" required />
      <button type="submit" disabled={mutation.isPending}>
        {mutation.isPending ? 'Creating...' : 'Create User'}
      </button>
      {mutation.isError && <p>Error: {mutation.error.message}</p>}
    </form>
  );
}

核心概念

服务端状态与客户端状态

服务端状态的特征:

  • 远程持久化(数据库、API、云端)
  • 获取或更新需要使用异步 API
  • 可能与客户端不同步
  • 可能被其他用户或系统更新
  • 示例:用户数据、帖子、产品、设置

客户端状态的特征:

  • 本地持久化(内存、localStorage)
  • 可同步访问
  • 完全由客户端控制
  • 示例:UI 主题、模态框的开关状态、表单输入

TanStack Query 管理服务端状态。客户端状态请使用 Zustand/Context。

查询键

查询键可唯一标识查询及其缓存数据。

键结构:

// String key (simple)
queryKey: ['todos']

// Array key (recommended for dependencies)
queryKey: ['todo', todoId]
queryKey: ['todos', { status: 'active', page: 1 }]

// Nested arrays (complex hierarchies)
queryKey: ['users', userId, 'posts', { sort: 'date' }]

键匹配:

// Exact match
queryClient.invalidateQueries({ queryKey: ['todos', 1], exact: true });

// Prefix match (invalidates all matching)
queryClient.invalidateQueries({ queryKey: ['todos'] }); // Matches ['todos', 1], ['todos', 2], etc.

// Predicate match
queryClient.invalidateQueries({
  predicate: (query) => query.queryKey[0] === 'todos' && query.state.data?.status === 'draft'
});

最佳实践:

  • 使用具有层级结构的数组:['resource', id, 'subresource']
  • 将变量放在末尾:['users', { filter, sort }]
  • 在各组件间保持顺序一致
  • 复杂参数使用对象
查询生命周期
FRESH → STALE → INACTIVE → GARBAGE COLLECTED
  ↓       ↓         ↓              ↓
  0ms   staleTime  no observers  cacheTime

状态:

  • Fresh(新鲜):数据被视为最新(处于 staleTime 范围内)
  • Stale(过期):数据可能已过期,将在触发时重新获取
  • Inactive(未活跃):没有组件在使用此查询
  • Garbage Collected(已垃圾回收):cacheTime 结束后从缓存中移除

配置:

useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  staleTime: 5 * 60 * 1000,     // 5 minutes (data fresh)
  gcTime: 10 * 60 * 1000,       // 10 minutes (cache retention)
  refetchOnWindowFocus: true,    // Refetch when window regains focus
  refetchOnReconnect: true,      // Refetch when reconnecting
  refetchInterval: 30000,        // Poll every 30 seconds
});
缓存行为

自动缓存:

// First component - triggers fetch
function ComponentA() {
  const { data } = useQuery({ queryKey: ['user', 1], queryFn: fetchUser });
  return <div>{data?.name}</div>;
}

// Second component - uses cache instantly
function ComponentB() {
  const { data } = useQuery({ queryKey: ['user', 1], queryFn: fetchUser });
  return <div>{data?.email}</div>; // No second fetch!
}

过期时重新验证:

// Shows cached data immediately, refetches in background if stale
const { data, isRefetching } = useQuery({
  queryKey: ['posts'],
  queryFn: fetchPosts,
  staleTime: 60000, // Fresh for 1 minute
});

// data available from cache immediately
// isRefetching = true if background refetch happening

查询

useQuery Hook

基本语法:

const {
  data,           // Query result
  error,          // Error object if failed
  isLoading,      // First load (no cached data)
  isFetching,     // Any fetch (including background)
  isSuccess,      // Query succeeded
  isError,        // Query failed
  status,         // 'pending' | 'error' | 'success'
  fetchStatus,    // 'fetching' | 'paused' | 'idle'
  refetch,        // Manual refetch function
} = useQuery({
  queryKey: ['key'],
  queryFn: async () => { /* fetch logic */ },
});
查询函数模式

基本获取:

const { data } = useQuery({
  queryKey: ['users'],
  queryFn: async () => {
    const response = await fetch('/api/users');
    if (!response.ok) throw new Error('Network error');
    return response.json();
  },
});

在函数中使用查询键:

const { data } = useQuery({
  queryKey: ['user', userId],
  queryFn: async ({ queryKey }) => {
    const [_key, userId] = queryKey;
    const response = await fetch(`/api/users/${userId}`);
    return response.json();
  },
});

中止信号(取消):

const { data } = useQuery({
  queryKey: ['todos'],
  queryFn: async ({ signal }) => {
    const response = await fetch('/api/todos', { signal });
    return response.json();
  },
});
// Automatically cancels on unmount or when query becomes inactive

Axios 模式:

import axios from 'axios';

const { data } = useQuery({
  queryKey: ['repos', username],
  queryFn: ({ signal }) =>
    axios.get(`/api/repos/${username}`, { signal }).then(res => res.data),
});
依赖查询

顺序查询:

// Wait for user before fetching projects
const { data: user } = useQuery({
  queryKey: ['user', userId],
  queryFn: () => fetchUser(userId),
});

const { data: projects } = useQuery({
  queryKey: ['projects', user?.id],
  queryFn: () => fetchProjects(user!.id),
  enabled: !!user, // Only run when user exists
});

条件查询:

const { data } = useQuery({
  queryKey: ['premium-features', userId],
  queryFn: fetchPremiumFeatures,
  enabled: user?.isPremium === true, // Only fetch for premium users
});
并行查询

手动并行:

function Dashboard() {
  const users = useQuery({ queryKey: ['users'], queryFn: fetchUsers });
  const posts = useQuery({ queryKey: ['posts'], queryFn: fetchPosts });
  const projects = useQuery({ queryKey: ['projects'], queryFn: fetchProjects });

  if (users.isLoading || posts.isLoading || projects.isLoading) {
    return <Spinner />;
  }

  return <div>/* render dashboard */</div>;
}

useQueries(动态并行):

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

function MultiUserProfiles({ userIds }: { userIds: number[] }) {
  const results = useQueries({
    queries: userIds.map(id => ({
      queryKey: ['user', id],
      queryFn: () => fetchUser(id),
      staleTime: 60000,
    })),
  });

  const allLoaded = results.every(r => r.isSuccess);

  if (!allLoaded) return <Spinner />;

  return (
    <div>
      {results.map((result, i) => (
        <UserCard key={userIds[i]} user={result.data} />
      ))}
    </div>
  );
}
查询占位内容

占位数据(即时 UI):

const { data } = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  placeholderData: [], // Show empty array while loading
});

// Dynamic placeholder from cache
const { data } = useQuery({
  queryKey: ['todo', id],
  queryFn: () => fetchTodo(id),
  placeholderData: () => {
    // Use cached list to find placeholder
    return queryClient
      .getQueryData(['todos'])
      ?.find(d => d.id === id);
  },
});

初始数据(水合):

const { data } = useQuery({
  queryKey: ['todo', id],
  queryFn: () => fetchTodo(id),
  initialData: () => {
    return queryClient
      .getQueryData(['todos'])
      ?.find(d => d.id === id);
  },
  initialDataUpdatedAt: () =>
    queryClient.getQueryState(['todos'])?.dataUpdatedAt,
});

区别:

  • placeholderData:不会持久化到缓存,仅用于 UI
  • initialData:作为真实数据持久化到缓存

变更

useMutation Hook

基本变更:

const mutation = useMutation({
  mutationFn: async (newTodo: Todo) => {
    const response = await fetch('/api/todos', {
      method: 'POST',
      body: JSON.stringify(newTodo),
    });
    return response.json();
  },
  onSuccess: (data) => {
    console.log('Created:', data);
  },
  onError: (error) => {
    console.error('Failed:', error);
  },
});

// Trigger mutation
mutation.mutate({ title: 'New Todo', done: false });

// Async/await variant
try {
  const data = await mutation.mutateAsync(newTodo);
  console.log(data);
} catch (error) {
  console.error(error);
}

变更状态:

const {
  mutate,          // Trigger function
  mutateAsync,     // Promise variant
  data,            // Result from successful mutation
  error,           // Error from failed mutation
  isPending,       // Mutation in progress
  isSuccess,       // Mutation succeeded
  isError,         // Mutation failed
  reset,           // Reset mutation state
} = useMutation({ /* ... */ });
缓存失效

变更后使查询失效:

const mutation = useMutation({
  mutationFn: createTodo,
  onSuccess: () => {
    // Refetch all 'todos' queries
    queryClient.invalidateQueries({ queryKey: ['todos'] });
  },
});

使多个查询失效:

const mutation = useMutation({
  mutationFn: updateUser,
  onSuccess: (data, variables) => {
    // Invalidate multiple query families
    queryClient.invalidateQueries({ queryKey: ['user', variables.id] });
    queryClient.invalidateQueries({ queryKey: ['users'] });
    queryClient.invalidateQueries({ queryKey: ['teams', data.teamId] });
  },
});

选择性失效:

// Only invalidate specific queries
queryClient.invalidateQueries({
  queryKey: ['todos'],
  exact: true, // Only ['todos'], not ['todos', 1]
});

// Predicate-based invalidation
queryClient.invalidateQueries({
  predicate: (query) =>
    query.queryKey[0] === 'todos' &&
    query.state.data?.status === 'draft',
});
手动更新缓存

setQueryData(直接更新):

const mutation = useMutation({
  mutationFn: updateTodo,
  onSuccess: (updatedTodo) => {
    // Update specific todo in cache
    queryClient.setQueryData(
      ['todo', updatedTodo.id],
      updatedTodo
    );

    // Update todo in list
    queryClient.setQueryData(['todos'], (old: Todo[] = []) =>
      old.map(todo =>
        todo.id === updatedTodo.id ? updatedTodo : todo
      )
    );
  },
});

不可变更新:

// Add to list
queryClient.setQueryData(['todos'], (old: Todo[] = []) =>
  [...old, newTodo]
);

// Remove from list
queryClient.setQueryData(['todos'], (old: Todo[] = []) =>
  old.filter(todo => todo.id !== deletedId)
);

// Update in list
queryClient.setQueryData(['todos'], (old: Todo[] = []) =>
  old.map(todo => todo.id === id ? { ...todo, ...updates } : todo)
);

乐观更新

基本乐观更新
const mutation = useMutation({
  mutationFn: updateTodo,
  // Before mutation executes
  onMutate: async (newTodo) => {
    // Cancel outgoing refetches
    await queryClient.cancelQueries({ queryKey: ['todos'] });

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

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

    // Return context with snapshot
    return { previousTodos };
  },
  // On error, rollback
  onError: (err, newTodo, context) => {
    queryClient.setQueryData(['todos'], context?.previousTodos);
  },
  // Always refetch after success or error
  onSettled: () => {
    queryClient.invalidateQueries({ queryKey: ['todos'] });
  },
});
复杂乐观更新模式
interface Todo {
  id: number;
  title: string;
  done: boolean;
}

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

  return useMutation({
    mutationFn: async (updatedTodo: Todo) => {
      const response = await fetch(`/api/todos/${updatedTodo.id}`, {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(updatedTodo),
      });
      if (!response.ok) throw new Error('Update failed');
      return response.json();
    },

    onMutate: async (updatedTodo) => {
      // Cancel queries to prevent race conditions
      await queryClient.cancelQueries({ queryKey: ['todos'] });
      await queryClient.cancelQueries({ queryKey: ['todo', updatedTodo.id] });

      // Snapshot current state
      const previousTodos = queryClient.getQueryData<Todo[]>(['todos']);
      const previousTodo = queryClient.getQueryData<Todo>(['todo', updatedTodo.id]);

      // Optimistically update list
      if (previousTodos) {
        queryClient.setQueryData<Todo[]>(['todos'], (old = []) =>
          old.map(todo => todo.id === updatedTodo.id ? updatedTodo : todo)
        );
      }

      // Optimistically update detail
      queryClient.setQueryData(['todo', updatedTodo.id], updatedTodo);

      return { previousTodos, previousTodo };
    },

    onError: (err, updatedTodo, context) => {
      // Rollback on error
      if (context?.previousTodos) {
        queryClient.setQueryData(['todos'], context.previousTodos);
      }
      if (context?.previousTodo) {
        queryClient.setQueryData(['todo', updatedTodo.id], context.previousTodo);
      }
    },

    onSettled: (data, error, variables) => {
      // Always refetch to ensure sync
      queryClient.invalidateQueries({ queryKey: ['todos'] });
      queryClient.invalidateQueries({ queryKey: ['todo', variables.id] });
    },
  });
};

// Usage
function TodoItem({ todo }: { todo: Todo }) {
  const updateTodo = useUpdateTodo();

  const toggleDone = () => {
    updateTodo.mutate({ ...todo, done: !todo.done });
  };

  return (
    <div>
      <input
        type="checkbox"
        checked={todo.done}
        onChange={toggleDone}
        disabled={updateTodo.isPending}
      />
      {todo.title}
    </div>
  );
}

分页

useInfiniteQuery(无限滚动)

基本的无限查询:

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

interface PostsResponse {
  posts: Post[];
  nextCursor?: number;
}

function InfinitePosts() {
  const {
    data,
    error,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
  } = useInfiniteQuery({
    queryKey: ['posts'],
    queryFn: async ({ pageParam = 0 }) => {
      const response = await fetch(`/api/posts?cursor=${pageParam}`);
      return response.json() as Promise<PostsResponse>;
    },
    getNextPageParam: (lastPage) => lastPage.nextCursor,
    initialPageParam: 0,
  });

  return (
    <div>
      {data?.pages.map((page, i) => (
        <div key={i}>
          {page.posts.map(post => (
            <PostCard key={post.id} post={post} />
          ))}
        </div>
      ))}

      <button
        onClick={() => fetchNextPage()}
        disabled={!hasNextPage || isFetchingNextPage}
      >
        {isFetchingNextPage
          ? 'Loading more...'
          : hasNextPage
          ? 'Load More'
          : 'Nothing more to load'}
      </button>
    </div>
  );
}

双向分页:

const {
  data,
  fetchNextPage,
  fetchPreviousPage,
  hasNextPage,
  hasPreviousPage,
} = useInfiniteQuery({
  queryKey: ['posts'],
  queryFn: async ({ pageParam = 0 }) => {
    const response = await fetch(`/api/posts?cursor=${pageParam}`);
    return response.json();
  },
  getNextPageParam: (lastPage) => lastPage.nextCursor,
  getPreviousPageParam: (firstPage) => firstPage.prevCursor,
  initialPageParam: 0,
});

使用 Intersection Observer 实现无限滚动:

import { useInfiniteQuery } from '@tanstack/react-query';
import { useInView } from 'react-intersection-observer';
import { useEffect } from 'react';

function AutoLoadPosts() {
  const { ref, inView } = useInView();

  const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
  } = useInfiniteQuery({
    queryKey: ['posts'],
    queryFn: fetchPosts,
    getNextPageParam: (lastPage) => lastPage.nextCursor,
    initialPageParam: 0,
  });

  // Auto-fetch when sentinel comes into view
  useEffect(() => {
    if (inView && hasNextPage) {
      fetchNextPage();
    }
  }, [inView, hasNextPage, fetchNextPage]);

  return (
    <div>
      {data?.pages.map((page, i) => (
        <div key={i}>
          {page.posts.map(post => <PostCard key={post.id} post={post} />)}
        </div>
      ))}

      {/* Sentinel element */}
      <div ref={ref}>
        {isFetchingNextPage && <Spinner />}
      </div>
    </div>
  );
}
传统分页

基于页码的分页:

function PaginatedPosts() {
  const [page, setPage] = useState(1);

  const { data, isLoading } = useQuery({
    queryKey: ['posts', page],
    queryFn: () => fetchPosts(page),
    placeholderData: (previousData) => previousData, // Keep previous data while loading
  });

  return (
    <div>
      {isLoading ? (
        <Spinner />
      ) : (
        <div>
          {data.posts.map(post => <PostCard key={post.id} post={post} />)}
        </div>
      )}

      <div>
        <button
          onClick={() => setPage(old => Math.max(old - 1, 1))}
          disabled={page === 1}
        >
          Previous
        </button>
        <span>Page {page}</span>
        <button
          onClick={() => setPage(old => old + 1)}
          disabled={!data?.hasMore}
        >
          Next
        </button>
      </div>
    </div>
  );
}

预取下一页:

function PaginatedPosts() {
  const queryClient = useQueryClient();
  const [page, setPage] = useState(1);

  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]);

  return (
    <div>
      {/* ... */}
    </div>
  );
}

缓存管理

Query Client 方法

getQueryData(读取缓存):

const todos = queryClient.getQueryData<Todo[]>(['todos']);
const user = queryClient.getQueryData<User>(['user', userId]);

setQueryData(写入缓存):

queryClient.setQueryData(['user', 1], newUser);

// Updater function
queryClient.setQueryData<Todo[]>(['todos'], (old = []) => [...old, newTodo]);

invalidateQueries(标记为过期并重新获取):

// Invalidate all queries
queryClient.invalidateQueries();

// Invalidate by key prefix
queryClient.invalidateQueries({ queryKey: ['todos'] });

// Exact match only
queryClient.invalidateQueries({ queryKey: ['todos'], exact: true });

// With refetch control
queryClient.invalidateQueries({
  queryKey: ['todos'],
  refetchType: 'active', // 'active' | 'inactive' | 'all' | 'none'
});

refetchQueries(立即重新获取):

// Refetch all active queries
await queryClient.refetchQueries();

// Refetch specific queries
await queryClient.refetchQueries({ queryKey: ['todos'] });

// Refetch with filters
await queryClient.refetchQueries({
  queryKey: ['todos'],
  type: 'active', // Only refetch active queries
});

removeQueries(从缓存中删除):

// Remove all queries
queryClient.removeQueries();

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

// Remove with predicate
queryClient.removeQueries({
  predicate: (query) =>
    query.queryKey[0] === 'todos' &&
    query.state.data?.isArchived === true,
});

resetQueries(重置为初始状态):

// Reset all queries
queryClient.resetQueries();

// Reset specific
queryClient.resetQueries({ queryKey: ['todos'] });
缓存配置

全局默认值:

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

单个查询的配置:

useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  staleTime: Infinity,      // Never mark stale
  gcTime: Infinity,         // Never garbage collect
  refetchInterval: 5000,    // Refetch every 5s
  refetchIntervalInBackground: false, // Don't refetch when tab inactive
});
缓存持久化

持久化到 LocalStorage:

import { QueryClient } from '@tanstack/react-query';
import { persistQueryClient } from '@tanstack/react-query-persist-client';
import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister';

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      gcTime: 1000 * 60 * 60 * 24, // 24 hours
    },
  },
});

const persister = createSyncStoragePersister({
  storage: window.localStorage,
});

persistQueryClient({
  queryClient,
  persister,
  maxAge: 1000 * 60 * 60 * 24, // 24 hours
});

IndexedDB 持久化:

import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister';
import { get, set, del } from 'idb-keyval';

const persister = createAsyncStoragePersister({
  storage: {
    getItem: async (key) => await get(key),
    setItem: async (key, value) => await set(key, value),
    removeItem: async (key) => await del(key),
  },
});

错误处理与重试

错误处理

查询错误边界:

import { QueryErrorResetBoundary } from '@tanstack/react-query';
import { ErrorBoundary } from 'react-error-boundary';

function App() {
  return (
    <QueryErrorResetBoundary>
      {({ reset }) => (
        <ErrorBoundary
          onReset={reset}
          fallbackRender={({ error, resetErrorBoundary }) => (
            <div>
              <p>Error: {error.message}</p>
              <button onClick={resetErrorBoundary}>Try again</button>
            </div>
          )}
        >
          <Component />
        </ErrorBoundary>
      )}
    </QueryErrorResetBoundary>
  );
}

// Component throws errors to boundary
function Component() {
  const { data } = useQuery({
    queryKey: ['user'],
    queryFn: fetchUser,
    throwOnError: true, // Throw errors to error boundary
  });
  return <div>{data.name}</div>;
}

自定义错误类型:

class APIError extends Error {
  constructor(
    message: string,
    public status: number,
    public code?: string
  ) {
    super(message);
    this.name = 'APIError';
  }
}

const { error } = useQuery({
  queryKey: ['user'],
  queryFn: async () => {
    const response = await fetch('/api/user');
    if (!response.ok) {
      throw new APIError(
        'Failed to fetch user',
        response.status,
        await response.text()
      );
    }
    return response.json();
  },
});

if (error instanceof APIError) {
  if (error.status === 404) return <NotFound />;
  if (error.status === 401) return <Unauthorized />;
}
重试逻辑

默认重试:

// Retries 3 times with exponential backoff
useQuery({
  queryKey: ['data'],
  queryFn: fetchData,
  retry: 3, // Number of retries
  retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),
});

条件重试:

useQuery({
  queryKey: ['data'],
  queryFn: fetchData,
  retry: (failureCount, error) => {
    // Don't retry on 404
    if (error instanceof APIError && error.status === 404) {
      return false;
    }
    // Retry up to 3 times for other errors
    return failureCount < 3;
  },
});

Mutation 重试:

useMutation({
  mutationFn: createUser,
  retry: 2, // Retry mutations (use sparingly)
  retryDelay: 1000,
});
网络状态检测

在线/离线处理:

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      networkMode: 'offlineFirst', // 'online' | 'always' | 'offlineFirst'
      refetchOnReconnect: true,
    },
  },
});

// Custom online/offline indicator
function OnlineStatus() {
  const queryClient = useQueryClient();
  const isOnline = useOnlineManager().isOnline();

  useEffect(() => {
    if (isOnline) {
      queryClient.refetchQueries();
    }
  }, [isOnline, queryClient]);

  return isOnline ? <OnlineIcon /> : <OfflineIcon />;
}

SSR 与水合

Next.js App Router

服务端组件数据获取:

// app/users/page.tsx
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query';
import { UsersList } from './UsersList';

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

  // Prefetch on server
  await queryClient.prefetchQuery({
    queryKey: ['users'],
    queryFn: fetchUsers,
  });

  return (
    <HydrationBoundary state={dehydrate(queryClient)}>
      <UsersList />
    </HydrationBoundary>
  );
}

客户端组件:

// app/users/UsersList.tsx
'use client';

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

export function UsersList() {
  // Uses hydrated data from server
  const { data } = useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers,
  });

  return (
    <ul>
      {data?.map(user => <li key={user.id}>{user.name}</li>)}
    </ul>
  );
}
Next.js Pages Router

getServerSideProps:

import { dehydrate, QueryClient } from '@tanstack/react-query';

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

  await queryClient.prefetchQuery({
    queryKey: ['users'],
    queryFn: fetchUsers,
  });

  return {
    props: {
      dehydratedState: dehydrate(queryClient),
    },
  };
}

function UsersPage() {
  const { data } = useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers,
  });

  return <div>{/* ... */}</div>;
}

export default UsersPage;

_app.tsx 设置:

// pages/_app.tsx
import { useState } from 'react';
import { QueryClient, QueryClientProvider, HydrationBoundary } from '@tanstack/react-query';

export default function App({ Component, pageProps }) {
  const [queryClient] = useState(() => new QueryClient());

  return (
    <QueryClientProvider client={queryClient}>
      <HydrationBoundary state={pageProps.dehydratedState}>
        <Component {...pageProps} />
      </HydrationBoundary>
    </QueryClientProvider>
  );
}
流式 SSR

Suspense 集成:

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

function UserProfile({ userId }: { userId: number }) {
  const { data } = useSuspenseQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
  });

  // No loading state needed - Suspense handles it
  return <div>{data.name}</div>;
}

// In parent component
<Suspense fallback={<Spinner />}>
  <UserProfile userId={1} />
</Suspense>

集成模式

tRPC 集成

设置:

// utils/trpc.ts
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '@/server/routers/_app';

export const trpc = createTRPCReact<AppRouter>();

Provider:

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

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { httpBatchLink } from '@trpc/client';
import { useState } from 'react';
import { trpc } from '@/utils/trpc';

export function Providers({ children }: { children: React.ReactNode }) {
  const [queryClient] = useState(() => new QueryClient());
  const [trpcClient] = useState(() =>
    trpc.createClient({
      links: [
        httpBatchLink({
          url: '/api/trpc',
        }),
      ],
    })
  );

  return (
    <trpc.Provider client={trpcClient} queryClient={queryClient}>
      <QueryClientProvider client={queryClient}>
        {children}
      </QueryClientProvider>
    </trpc.Provider>
  );
}

用法:

function UserProfile() {
  // Query
  const { data } = trpc.user.getById.useQuery({ id: 1 });

  // Mutation
  const utils = trpc.useUtils();
  const mutation = trpc.user.create.useMutation({
    onSuccess: () => {
      utils.user.list.invalidate();
    },
  });

  return <div>{data?.name}</div>;
}
使用 Axios 调用 REST API

API 客户端:

// lib/api-client.ts
import axios from 'axios';

export const apiClient = axios.create({
  baseURL: process.env.NEXT_PUBLIC_API_URL,
  headers: {
    'Content-Type': 'application/json',
  },
});

// Request interceptor
apiClient.interceptors.request.use(config => {
  const token = localStorage.getItem('token');
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

// Response interceptor
apiClient.interceptors.response.use(
  response => response,
  error => {
    if (error.response?.status === 401) {
      // Handle unauthorized
      window.location.href = '/login';
    }
    return Promise.reject(error);
  }
);

查询钩子:

// hooks/useUsers.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { apiClient } from '@/lib/api-client';

export function useUsers() {
  return useQuery({
    queryKey: ['users'],
    queryFn: async ({ signal }) => {
      const { data } = await apiClient.get('/users', { signal });
      return data;
    },
  });
}

export function useUser(id: number) {
  return useQuery({
    queryKey: ['user', id],
    queryFn: async ({ signal }) => {
      const { data } = await apiClient.get(`/users/${id}`, { signal });
      return data;
    },
    enabled: !!id,
  });
}

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

  return useMutation({
    mutationFn: (newUser: NewUser) =>
      apiClient.post('/users', newUser).then(res => res.data),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['users'] });
    },
  });
}
GraphQL 集成

Apollo Client 替代方案:

import { useQuery } from '@tanstack/react-query';
import { request, gql } from 'graphql-request';

const endpoint = 'https://api.example.com/graphql';

const GET_USERS = gql`
  query GetUsers {
    users {
      id
      name
      email
    }
  }
`;

function useUsers() {
  return useQuery({
    queryKey: ['users'],
    queryFn: async () => request(endpoint, GET_USERS),
  });
}

// With variables
const GET_USER = gql`
  query GetUser($id: ID!) {
    user(id: $id) {
      id
      name
      email
    }
  }
`;

function useUser(id: string) {
  return useQuery({
    queryKey: ['user', id],
    queryFn: async () => request(endpoint, GET_USER, { id }),
  });
}
使用 Zustand 管理全局状态

组合模式:

// store/useAuthStore.ts
import { create } from 'zustand';

interface AuthState {
  token: string | null;
  setToken: (token: string | null) => void;
  logout: () => void;
}

export const useAuthStore = create<AuthState>((set) => ({
  token: localStorage.getItem('token'),
  setToken: (token) => {
    if (token) {
      localStorage.setItem('token', token);
    } else {
      localStorage.removeItem('token');
    }
    set({ token });
  },
  logout: () => {
    localStorage.removeItem('token');
    set({ token: null });
  },
}));

// hooks/useAuthenticatedQuery.ts
import { useQuery } from '@tanstack/react-query';
import { useAuthStore } from '@/store/useAuthStore';

export function useAuthenticatedQuery() {
  const token = useAuthStore(state => state.token);

  return useQuery({
    queryKey: ['profile', token],
    queryFn: async () => {
      const response = await fetch('/api/profile', {
        headers: { Authorization: `Bearer ${token}` },
      });
      return response.json();
    },
    enabled: !!token,
  });
}

TypeScript 模式

类型化查询

泛型查询钩子:

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

// Explicit typing
const { data } = useQuery<User, Error>({
  queryKey: ['user', id],
  queryFn: async () => {
    const response = await fetch(`/api/users/${id}`);
    return response.json(); // TypeScript infers return type
  },
});

// data is User | undefined
// error is Error | null

类型安全的查询键:

// Define query keys with types
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: number) => [...userKeys.details(), id] as const,
};

// Usage with full type safety
const { data } = useQuery({
  queryKey: userKeys.detail(userId),
  queryFn: () => fetchUser(userId),
});

// Invalidate with autocomplete
queryClient.invalidateQueries({ queryKey: userKeys.lists() });

带类型的自定义钩子:

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

interface UseUserOptions {
  enabled?: boolean;
  onSuccess?: (user: User) => void;
}

function useUser(id: number, options?: UseUserOptions) {
  return useQuery({
    queryKey: ['user', id],
    queryFn: async (): Promise<User> => {
      const response = await fetch(`/api/users/${id}`);
      if (!response.ok) throw new Error('Failed to fetch user');
      return response.json();
    },
    enabled: options?.enabled,
    // Type-safe callbacks
    onSuccess: options?.onSuccess,
  });
}

// Usage
const { data } = useUser(1, {
  enabled: true,
  onSuccess: (user) => {
    console.log(user.name); // TypeScript knows user is User
  },
});
类型化 Mutation
interface CreateUserPayload {
  name: string;
  email: string;
}

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

function useCreateUser() {
  return useMutation<User, Error, CreateUserPayload>({
    mutationFn: async (payload) => {
      const response = await fetch('/api/users', {
        method: 'POST',
        body: JSON.stringify(payload),
      });
      return response.json();
    },
    onSuccess: (data) => {
      // data is User
      console.log('Created user:', data.name);
    },
    onError: (error) => {
      // error is Error
      console.error('Failed:', error.message);
    },
  });
}

// Usage
const mutation = useCreateUser();
mutation.mutate({ name: 'John', email: '[email protected]' });
Query Client 类型
import { QueryClient } from '@tanstack/react-query';

// Type-safe query client methods
const user = queryClient.getQueryData<User>(['user', 1]);

queryClient.setQueryData<User>(['user', 1], (old) => {
  // old is User | undefined
  if (!old) return old;
  return { ...old, name: 'Updated' };
});

// Type-safe invalidation
queryClient.invalidateQueries<User>({
  queryKey: ['users'],
  predicate: (query) => {
    // query.state.data is User | undefined
    return query.state.data?.isActive === true;
  },
});

测试

设置测试环境

测试工具:

// test/utils.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { render } from '@testing-library/react';
import { ReactNode } from 'react';

export function createTestQueryClient() {
  return new QueryClient({
    defaultOptions: {
      queries: {
        retry: false, // Don't retry failed queries in tests
        gcTime: Infinity,
      },
    },
    logger: {
      log: console.log,
      warn: console.warn,
      error: () => {}, // Silence errors in tests
    },
  });
}

export function renderWithClient(ui: ReactNode) {
  const testQueryClient = createTestQueryClient();

  return render(
    <QueryClientProvider client={testQueryClient}>
      {ui}
    </QueryClientProvider>
  );
}
测试查询

基本查询测试:

// UserProfile.test.tsx
import { renderWithClient } from '@/test/utils';
import { screen, waitFor } from '@testing-library/react';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { UserProfile } from './UserProfile';

const server = setupServer(
  rest.get('/api/users/1', (req, res, ctx) => {
    return res(
      ctx.json({
        id: 1,
        name: 'John Doe',
        email: '[email protected]',
      })
    );
  })
);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

test('displays user profile', async () => {
  renderWithClient(<UserProfile userId={1} />);

  expect(screen.getByText('Loading...')).toBeInTheDocument();

  await waitFor(() => {
    expect(screen.getByText('John Doe')).toBeInTheDocument();
  });
});

test('handles fetch error', async () => {
  server.use(
    rest.get('/api/users/1', (req, res, ctx) => {
      return res(ctx.status(500));
    })
  );

  renderWithClient(<UserProfile userId={1} />);

  await waitFor(() => {
    expect(screen.getByText(/error/i)).toBeInTheDocument();
  });
});
测试 Mutation
// CreateUserForm.test.tsx
import { renderWithClient } from '@/test/utils';
import { screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { CreateUserForm } from './CreateUserForm';

const server = setupServer(
  rest.post('/api/users', async (req, res, ctx) => {
    const body = await req.json();
    return res(
      ctx.json({
        id: 1,
        ...body,
      })
    );
  })
);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

test('creates user successfully', async () => {
  const user = userEvent.setup();
  renderWithClient(<CreateUserForm />);

  await user.type(screen.getByPlaceholderText('Name'), 'John Doe');
  await user.type(screen.getByPlaceholderText('Email'), '[email protected]');
  await user.click(screen.getByRole('button', { name: /create/i }));

  await waitFor(() => {
    expect(screen.getByText(/created successfully/i)).toBeInTheDocument();
  });
});
使用模拟数据测试

水合查询数据:

test('renders with initial data', () => {
  const testQueryClient = createTestQueryClient();

  // Pre-populate cache
  testQueryClient.setQueryData(['user', 1], {
    id: 1,
    name: 'John Doe',
    email: '[email protected]',
  });

  render(
    <QueryClientProvider client={testQueryClient}>
      <UserProfile userId={1} />
    </QueryClientProvider>
  );

  // Data immediately available (no loading state)
  expect(screen.getByText('John Doe')).toBeInTheDocument();
});
测试自定义钩子
// useUser.test.ts
import { renderHook, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { useUser } from './useUser';

const server = setupServer(
  rest.get('/api/users/:id', (req, res, ctx) => {
    return res(ctx.json({ id: 1, name: 'John Doe' }));
  })
);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

test('fetches user data', async () => {
  const queryClient = new QueryClient();
  const wrapper = ({ children }: { children: React.ReactNode }) => (
    <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
  );

  const { result } = renderHook(() => useUser(1), { wrapper });

  await waitFor(() => expect(result.current.isSuccess).toBe(true));

  expect(result.current.data).toEqual({ id: 1, name: 'John Doe' });
});

性能优化

查询去重

自动去重:

// Multiple components request same data - only one network request
function Dashboard() {
  return (
    <div>
      <UserStats userId={1} />    {/* Triggers fetch */}
      <UserProfile userId={1} />  {/* Uses cache */}
      <UserActivity userId={1} /> {/* Uses cache */}
    </div>
  );
}
预取

悬停时预取:

function UserLink({ userId }: { userId: number }) {
  const queryClient = useQueryClient();

  const prefetchUser = () => {
    queryClient.prefetchQuery({
      queryKey: ['user', userId],
      queryFn: () => fetchUser(userId),
      staleTime: 60000,
    });
  };

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

路由预取:

// Next.js App Router
import { QueryClient, HydrationBoundary, dehydrate } from '@tanstack/react-query';

export default async function UserPage({ params }: { params: { id: string } }) {
  const queryClient = new QueryClient();

  // Prefetch user data
  await queryClient.prefetchQuery({
    queryKey: ['user', params.id],
    queryFn: () => fetchUser(params.id),
  });

  // Prefetch related data
  await queryClient.prefetchQuery({
    queryKey: ['user-posts', params.id],
    queryFn: () => fetchUserPosts(params.id),
  });

  return (
    <HydrationBoundary state={dehydrate(queryClient)}>
      <UserProfile userId={params.id} />
    </HydrationBoundary>
  );
}
选择和转换数据

记忆化选择器:

// Only re-render when selected data changes
function TodoList({ filter }: { filter: 'all' | 'done' | 'pending' }) {
  const { data: filteredTodos } = useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
    select: (todos) => {
      // This only runs when todos change
      if (filter === 'done') return todos.filter(t => t.done);
      if (filter === 'pending') return todos.filter(t => !t.done);
      return todos;
    },
  });

  // Component only re-renders when filteredTodos change
  return (
    <ul>
      {filteredTodos?.map(todo => <li key={todo.id}>{todo.title}</li>)}
    </ul>
  );
}

高开销计算:

const { data: sortedUsers } = useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,
  select: (users) => {
    // Heavy sorting only runs when users change
    return users
      .slice()
      .sort((a, b) => a.name.localeCompare(b.name));
  },
});
结构共享

自动结构共享:

// TanStack Query automatically does structural sharing
const { data } = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  structuralSharing: true, // Default
});

// If refetch returns identical data structure,
// component doesn't re-render even though fetch completed

自定义结构共享:

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

const { data } = useQuery({
  queryKey: ['data'],
  queryFn: fetchData,
  structuralSharing: (oldData, newData) => {
    // Custom comparison logic
    return replaceEqualDeep(oldData, newData);
  },
});
取消查询

中止正在进行的请求:

const { data, refetch } = useQuery({
  queryKey: ['search', searchTerm],
  queryFn: async ({ signal }) => {
    const response = await fetch(`/api/search?q=${searchTerm}`, {
      signal, // Pass abort signal
    });
    return response.json();
  },
});

// When searchTerm changes, previous request is cancelled automatically

手动取消:

const queryClient = useQueryClient();

// Cancel all queries
queryClient.cancelQueries();

// Cancel specific query
queryClient.cancelQueries({ queryKey: ['todos'] });

最佳实践与常见模式

查询键工厂

集中管理查询键:

// lib/query-keys.ts
export const queryKeys = {
  users: {
    all: ['users'] as const,
    lists: () => [...queryKeys.users.all, 'list'] as const,
    list: (filters: UserFilters) => [...queryKeys.users.lists(), filters] as const,
    details: () => [...queryKeys.users.all, 'detail'] as const,
    detail: (id: number) => [...queryKeys.users.details(), id] as const,
  },
  posts: {
    all: ['posts'] as const,
    lists: () => [...queryKeys.posts.all, 'list'] as const,
    list: (filters: PostFilters) => [...queryKeys.posts.lists(), filters] as const,
    details: () => [...queryKeys.posts.all, 'detail'] as const,
    detail: (id: number) => [...queryKeys.posts.details(), id] as const,
  },
};

// Usage
const { data } = useQuery({
  queryKey: queryKeys.users.detail(userId),
  queryFn: () => fetchUser(userId),
});

// Invalidate all user lists
queryClient.invalidateQueries({ queryKey: queryKeys.users.lists() });
自定义钩子模式

资源钩子工厂:

// lib/create-resource-hooks.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';

export function createResourceHooks<T, CreateT = Partial<T>, UpdateT = Partial<T>>(
  resourceName: string,
  api: {
    getAll: () => Promise<T[]>;
    getOne: (id: string | number) => Promise<T>;
    create: (data: CreateT) => Promise<T>;
    update: (id: string | number, data: UpdateT) => Promise<T>;
    delete: (id: string | number) => Promise<void>;
  }
) {
  const keys = {
    all: [resourceName] as const,
    lists: () => [...keys.all, 'list'] as const,
    details: () => [...keys.all, 'detail'] as const,
    detail: (id: string | number) => [...keys.details(), id] as const,
  };

  return {
    useList: () =>
      useQuery({
        queryKey: keys.lists(),
        queryFn: api.getAll,
      }),

    useDetail: (id: string | number) =>
      useQuery({
        queryKey: keys.detail(id),
        queryFn: () => api.getOne(id),
        enabled: !!id,
      }),

    useCreate: () => {
      const queryClient = useQueryClient();
      return useMutation({
        mutationFn: api.create,
        onSuccess: () => {
          queryClient.invalidateQueries({ queryKey: keys.lists() });
        },
      });
    },

    useUpdate: () => {
      const queryClient = useQueryClient();
      return useMutation({
        mutationFn: ({ id, data }: { id: string | number; data: UpdateT }) =>
          api.update(id, data),
        onSuccess: (_, { id }) => {
          queryClient.invalidateQueries({ queryKey: keys.detail(id) });
          queryClient.invalidateQueries({ queryKey: keys.lists() });
        },
      });
    },

    useDelete: () => {
      const queryClient = useQueryClient();
      return useMutation({
        mutationFn: api.delete,
        onSuccess: () => {
          queryClient.invalidateQueries({ queryKey: keys.lists() });
        },
      });
    },
  };
}

// Usage
const userHooks = createResourceHooks('users', userApi);

function UsersList() {
  const { data: users } = userHooks.useList();
  const createUser = userHooks.useCreate();
  const deleteUser = userHooks.useDelete();

  return (
    <div>
      {users?.map(user => (
        <div key={user.id}>
          {user.name}
          <button onClick={() => deleteUser.mutate(user.id)}>Delete</button>
        </div>
      ))}
    </div>
  );
}
错误处理模式

集中式错误处理器:

// lib/query-client.ts
import { QueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';

export const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      onError: (error) => {
        if (error instanceof APIError) {
          toast.error(`Error: ${error.message}`);
        }
      },
    },
    mutations: {
      onError: (error) => {
        toast.error(`Failed to save: ${error.message}`);
      },
    },
  },
});
从 SWR 迁移

从 SWR 迁移到 TanStack Query:

// Before (SWR)
import useSWR from 'swr';

function Profile() {
  const { data, error, mutate } = useSWR('/api/user', fetcher);

  if (error) return <div>Error</div>;
  if (!data) return <div>Loading...</div>;
  return <div>{data.name}</div>;
}

// After (TanStack Query)
import { useQuery, useQueryClient } from '@tanstack/react-query';

function Profile() {
  const { data, error, isLoading } = useQuery({
    queryKey: ['/api/user'],
    queryFn: () => fetcher('/api/user'),
  });

  const queryClient = useQueryClient();
  const invalidate = () => queryClient.invalidateQueries({ queryKey: ['/api/user'] });

  if (error) return <div>Error</div>;
  if (isLoading) return <div>Loading...</div>;
  return <div>{data.name}</div>;
}

对比:

  • useSWR(key, fetcher) → useQuery({ queryKey: [key], queryFn: fetcher })
  • mutate() → queryClient.invalidateQueries()
  • !data 表示加载中 → isLoading
  • useSWRConfig() → useQueryClient()

DevTools

设置 DevTools:

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

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

生产构建:

// DevTools are automatically excluded in production builds
// No need to conditionally render

DevTools 功能:

  • 查看所有查询及其状态
  • 检查查询数据和错误
  • 手动触发重新获取
  • 使查询失效
  • 查看查询时间线
  • 监控缓存大小
  • 调试网络瀑布图

常见陷阱

❌ 不要在组件内部创建 QueryClient:

// WRONG - Creates new client on every render
function App() {
  const queryClient = new QueryClient(); // ❌
  return <QueryClientProvider client={queryClient}>...</QueryClientProvider>;
}

// CORRECT - Stable client instance
function App() {
  const [queryClient] = useState(() => new QueryClient()); // ✅
  return <QueryClientProvider client={queryClient}>...</QueryClientProvider>;
}

❌ 不要在渲染时未经检查就使用查询数据:

// WRONG - data might be undefined
function UserProfile() {
  const { data } = useQuery({ queryKey: ['user'], queryFn: fetchUser });
  return <div>{data.name}</div>; // ❌ Crashes if data is undefined
}

// CORRECT - Handle loading state
function UserProfile() {
  const { data, isLoading } = useQuery({ queryKey: ['user'], queryFn: fetchUser });
  if (isLoading) return <Spinner />; // ✅
  return <div>{data.name}</div>;
}

❌ 不要忘记查询键是依赖项:

// WRONG - Missing dependency in query key
function UserPosts({ userId, filter }: Props) {
  const { data } = useQuery({
    queryKey: ['posts'], // ❌ Missing userId and filter
    queryFn: () => fetchUserPosts(userId, filter),
  });
}

// CORRECT - All dependencies in key
function UserPosts({ userId, filter }: Props) {
  const { data } = useQuery({
    queryKey: ['posts', userId, filter], // ✅
    queryFn: () => fetchUserPosts(userId, filter),
  });
}

❌ 不要直接修改查询数据:

// WRONG - Mutating cached data
const { data } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos });
data.push(newTodo); // ❌ Mutates cache directly

// CORRECT - Use setQueryData
queryClient.setQueryData(['todos'], (old = []) => [...old, newTodo]); // ✅

概要

TanStack Query 是 React 应用中用于管理服务端状态的行业标准解决方案。可用于 API 数据获取、缓存、同步和实时更新。它消除了手动状态管理所需的大量样板代码,并提供自动后台重新获取、乐观更新、分页和智能缓存管理等强大功能。

要点:

  • 使用 useQuery 获取数据并自动缓存
  • 使用 useMutation 执行创建/更新/删除操作
  • 查询键是缓存管理的基础
  • 执行变更后使查询失效,以保持 UI 同步
  • 利用乐观更新,即时反馈 UI
  • 使用 useInfiniteQuery 实现分页和无限滚动
  • 与 Zustand 搭配管理客户端状态
  • 与 tRPC、REST 和 GraphQL 无缝集成
  • 使用 TypeScript 为所有内容添加类型,确保完整的类型安全
  • 使用 MSW 进行测试,模拟真实的 API

渐进式加载模式:

  • 入门:快速开始和基本设置
  • 中级:查询、变更和缓存管理
  • 高级:乐观更新、SSR、集成和性能

如需更多资源,请访问官方文档。

相关技能

使用 Tanstack Query 时,以下技能有助于提升工作流效率:

  • react:用于集成 TanStack Query 的 React 钩子和模式
  • nextjs:将 TanStack Query 与 Next.js App Router 和 Server Components 搭配使用
  • zustand:用于互补的客户端状态管理(与 TanStack Query 搭配实现混合状态管理)
  • test-driven-development:测试查询、变更和缓存行为

[如果已部署在你的工具包中,可在这些技能中查阅完整文档]

评论

支持 Markdown