全面掌握 TanStack Query:现代 React 应用的数据管理利器

12月 24, 2025
React, Nextjs, Frontend, ByAI

什么是 TanStack Query?#

TanStack Query(原名 React Query)是一个强大的数据获取和状态管理库,专门用于处理服务器状态。它不是传统意义上的状态管理库(如 Redux 或 Zustand),而是专注于解决异步数据获取、缓存、同步和更新的问题。

核心优势#

  • 🚀 自动缓存:智能缓存策略减少不必要的请求

  • 🔄 后台同步:自动在后台更新过期数据

  • 性能优化:内置分页、无限加载、乐观更新

  • 🛠 开发者体验:强大的开发工具和极简的 API

  • 🔒 TypeScript 原生:完整的类型安全支持

安装与基础配置#

npm install @tanstack/react-query

基础设置#

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

// 创建 QueryClient 实例
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000, // 5分钟
      cacheTime: 10 * 60 * 1000, // 10分钟
    },
  },
});

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <YourApp />
      {/* 开发工具 - 强烈推荐 */}
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  );
}

核心概念详解#

1. Query(查询) - 数据获取#

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

// 定义查询函数
const fetchUsers = async () => {
  const response = await fetch('/api/users');
  if (!response.ok) {
    throw new Error('Network response was not ok');
  }
  return response.json();
};

function UsersList() {
  // 使用 useQuery hook
  const {
    data: users,
    error,
    isLoading,
    isError,
    isSuccess,
    refetch,
    isFetching,
  } = useQuery({
    queryKey: ['users'], // 查询的唯一标识
    queryFn: fetchUsers, // 查询函数
    staleTime: 1000 * 60 * 5, // 数据保鲜时间(5分钟)
    cacheTime: 1000 * 60 * 10, // 缓存时间(10分钟)
    retry: 3, // 失败重试次数
    retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),
  });

  if (isLoading) return <div>加载中...</div>;
  if (isError) return <div>错误: {error.message}</div>;

  return (
    <div>
      <button onClick={() => refetch()} disabled={isFetching}>
        {isFetching ? '刷新中...' : '刷新数据'}
      </button>
      {users.map(user => (
        <div key={user.id}>{user.name}</div>
      ))}
    </div>
  );
}

2. Mutation(变更) - 数据修改#

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

const createUser = async (userData) => {
  const response = await fetch('/api/users', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(userData),
  });
  if (!response.ok) throw new Error('创建用户失败');
  return response.json();
};

function CreateUserForm() {
  const queryClient = useQueryClient();
  const [formData, setFormData] = useState({ name: '', email: '' });

  const mutation = useMutation({
    mutationFn: createUser,
    onSuccess: (newUser) => {
      // 1. 使相关查询失效,触发重新获取
      queryClient.invalidateQueries({ queryKey: ['users'] });
      
      // 2. 或者直接更新缓存(更高效)
      queryClient.setQueryData(['users'], old => [...old, newUser]);
      
      // 重置表单
      setFormData({ name: '', email: '' });
    },
    onError: (error) => {
      console.error('创建失败:', error);
    },
    onSettled: () => {
      // 无论成功失败都会执行
      console.log('Mutation 完成');
    },
  });

  const handleSubmit = (e) => {
    e.preventDefault();
    mutation.mutate(formData);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={formData.name}
        onChange={e => setFormData({...formData, name: e.target.value})}
        placeholder="姓名"
      />
      <input
        value={formData.email}
        onChange={e => setFormData({...formData, email: e.target.value})}
        placeholder="邮箱"
      />
      <button type="submit" disabled={mutation.isLoading}>
        {mutation.isLoading ? '创建中...' : '创建用户'}
      </button>
    </form>
  );
}

高级特性实战#

1. 依赖查询(串行查询)#

function UserProfile({ userId }) {
  // 先获取用户基本信息
  const { data: user } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
  });

  // 依赖用户信息获取用户详情
  const { data: profile } = useQuery({
    queryKey: ['user-profile', userId],
    queryFn: () => fetchUserProfile(userId),
    enabled: !!user, // 只有用户信息存在时才执行
  });

  // 获取用户帖子
  const { data: posts } = useQuery({
    queryKey: ['user-posts', userId],
    queryFn: () => fetchUserPosts(userId),
    enabled: !!user, // 依赖用户信息
  });

  if (!user) return <div>加载用户信息...</div>;

  return (
    <div>
      <h1>{user.name}</h1>
      {profile && <p>{profile.bio}</p>}
      {posts && posts.map(post => <div key={post.id}>{post.title}</div>)}
    </div>
  );
}

2. 乐观更新 - 提升用户体验#

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

  const updateTodo = useMutation({
    mutationFn: (updatedTodo) => 
      fetch(`/api/todos/${updatedTodo.id}`, {
        method: 'PUT',
        body: JSON.stringify(updatedTodo),
      }),
    
    // 乐观更新:立即更新UI,如果失败则回滚
    onMutate: async (updatedTodo) => {
      // 取消正在进行的查询,避免冲突
      await queryClient.cancelQueries({ queryKey: ['todos'] });
      
      // 保存当前状态的快照
      const previousTodos = queryClient.getQueryData(['todos']);
      
      // 乐观更新缓存
      queryClient.setQueryData(['todos'], old => 
        old.map(todo => todo.id === updatedTodo.id ? updatedTodo : todo)
      );
      
      // 返回快照用于错误回滚
      return { previousTodos };
    },
    
    onError: (error, updatedTodo, context) => {
      // 出错时回滚到之前的状态
      queryClient.setQueryData(['todos'], context.previousTodos);
    },
    
    onSettled: () => {
      // 最终重新获取确保数据一致
      queryClient.invalidateQueries({ queryKey: ['todos'] });
    },
  });

  const toggleTodo = (todo) => {
    updateTodo.mutate({
      ...todo,
      completed: !todo.completed,
    });
  };

  // ... 组件渲染逻辑
}

3. 无限滚动与分页#

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

const fetchPosts = async ({ pageParam = 1 }) => {
  const response = await fetch(`/api/posts?page=${pageParam}&limit=10`);
  return response.json();
};

function InfinitePosts() {
  const {
    data,
    error,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
    status,
  } = useInfiniteQuery({
    queryKey: ['posts'],
    queryFn: fetchPosts,
    getNextPageParam: (lastPage) => lastPage.nextPage,
    getPreviousPageParam: (firstPage) => firstPage.prevPage,
  });

  if (status === 'loading') return <div>加载中...</div>;
  if (status === 'error') return <div>错误: {error.message}</div>;

  return (
    <div>
      {data.pages.map((page, pageIndex) => (
        <div key={pageIndex}>
          {page.posts.map(post => (
            <article key={post.id}>
              <h3>{post.title}</h3>
              <p>{post.content}</p>
            </article>
          ))}
        </div>
      ))}
      
      <button
        onClick={() => fetchNextPage()}
        disabled={!hasNextPage || isFetchingNextPage}
      >
        {isFetchingNextPage ? '加载更多...' : '加载更多'}
      </button>
    </div>
  );
}

实际项目最佳实践#

1. 自定义 Hook 封装#

// hooks/useUsers.js
export const useUsers = () => {
  return useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers,
    staleTime: 5 * 60 * 1000,
  });
};

export const useUser = (userId) => {
  return useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
    enabled: !!userId, // 只有 userId 存在时才查询
  });
};

export const useCreateUser = () => {
  const queryClient = useQueryClient();
  
  return useMutation({
    mutationFn: createUser,
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['users'] });
    },
  });
};

// 在组件中使用
function UserManagement() {
  const { data: users } = useUsers();
  const createUserMutation = useCreateUser();
  
  // ... 组件逻辑
}

2. 错误处理与加载状态统一管理#

// components/QueryBoundary.jsx
function QueryBoundary({ children, loading, error, loadingComponent, errorComponent }) {
  if (loading) return loadingComponent || <DefaultLoading />;
  if (error) return errorComponent || <DefaultError error={error} />;
  return children;
}

// components/DefaultError.jsx
function DefaultError({ error, onRetry }) {
  return (
    <div className="error-boundary">
      <h3>出错了</h3>
      <p>{error.message}</p>
      <button onClick={onRetry}>重试</button>
    </div>
  );
}

// 使用示例
function UserProfile({ userId }) {
  const { data, isLoading, error, refetch } = useUser(userId);
  
  return (
    <QueryBoundary
      loading={isLoading}
      error={error}
      loadingComponent={<UserProfileSkeleton />}
      errorComponent={<DefaultError error={error} onRetry={refetch} />}
    >
      <UserProfileContent user={data} />
    </QueryBoundary>
  );
}

3. Next.js 集成(服务端渲染)#

// pages/users.js
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({ dehydratedState }) {
  // 客户端会直接使用服务端预取的数据
  const { data: users } = useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers,
  });

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

性能优化技巧#

1. 查询去重与缓存优化#

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      // 相同查询在5秒内不会重复发起
      staleTime: 5000,
      // 缓存数据保留10分钟
      cacheTime: 600000,
      // 窗口失焦时重新获取数据
      refetchOnWindowFocus: true,
      // 网络重连时重新获取
      refetchOnReconnect: true,
      // 页面不可见时暂停查询
      refetchOnMount: true,
    },
  },
});

2. 选择性数据更新#

// 只更新特定数据,而不是整个列表
queryClient.setQueryData(['todos'], oldTodos => 
  oldTodos.map(todo => 
    todo.id === updatedTodo.id ? updatedTodo : todo
  )
);

// 或者只使特定查询失效
queryClient.invalidateQueries({ 
  queryKey: ['todos'], 
  predicate: query => 
    query.queryKey[0] === 'todos' && query.data.some(todo => todo.important)
});

常见问题与解决方案#

1. 循环依赖问题#

// ❌ 错误:在渲染中立即设置查询依赖
const { data: user } = useQuery({
  queryKey: ['user', userId],
  queryFn: () => fetchUser(userId),
  enabled: userId !== undefined, // 可能导致循环渲染
});

// ✅ 正确:使用稳定的依赖
const [userId, setUserId] = useState(undefined);
const { data: user } = useQuery({
  queryKey: ['user', userId],
  queryFn: () => fetchUser(userId),
  enabled: !!userId, // 明确的布尔值
});

2. 内存泄漏处理#

// 自动垃圾回收
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      cacheTime: 10 * 60 * 1000, // 10分钟后清理未使用的缓存
      staleTime: 5 * 60 * 1000,  // 5分钟后标记为过期
    },
  },
});

总结#

TanStack Query 是现代 React 应用中处理服务器状态的终极解决方案。通过智能缓存、自动同步和极简的 API,它极大地简化了数据管理复杂度。掌握 TanStack Query 不仅能提升开发效率,还能显著改善应用性能和用户体验。

关键收获:

  • 使用 useQuery处理数据获取,useMutation处理数据修改

  • 合理配置缓存策略平衡性能与数据新鲜度

  • 利用乐观更新提升用户体验

  • 封装自定义 Hook 提高代码复用性

  • 结合错误边界提供完善的错误处理

开始在你的项目中实践这些模式,你会发现数据管理变得前所未有的简单和高效!


本文代码示例基于 TanStack Query v4+,建议查看官方文档获取最新 API 变更。

本文共 2433 字,上次修改于 Dec 24, 2025,以 CC 署名-非商业性使用-禁止演绎 4.0 国际 协议进行许可。

相关文章

» 全面掌握 tRPC:端到端类型安全的下一代 API 框架

» 理解 useMemo、useCallback 和 memo

» WebRTC 介绍

» pm2 使用

» Prisma 和 Drizzle 对比