Back

在 React 中使用 TanStack Query 实现智能数据获取

在 React 中使用 TanStack Query 实现智能数据获取

如果你构建过与 API 交互的 React 应用,你一定知道这种模式:用 useEffect 进行获取,用 useState 存储数据,再用一个 useState 处理加载状态,可能还需要一个处理错误。不知不觉中,你就在管理一团乱麻般的状态,仅仅是为了显示一个用户列表。

这种手动方式虽然可行,但很脆弱。当用户离开页面后又返回时会发生什么?是重新获取数据?还是使用过期数据?网络失败时如何重试?这些不是边缘情况——它们是生产环境应用的现实需求。

TanStack Query(原 React Query)通过将服务器状态与客户端状态区别对待来解决这些问题。你无需命令式地获取数据,而是声明你需要什么,让库来处理缓存、同步和更新。本文将向你展示如何从手动数据获取转向更智能、声明式的方法,这种方法具有良好的扩展性。

核心要点

  • 用声明式的 useQuery hooks 替换手动的 useEffect + useState 模式,获得更清晰、更易维护的代码
  • 利用自动缓存、后台重新获取和请求去重来改善用户体验
  • 使用带有乐观更新的 mutations 创建响应迅速、感觉即时的 UI
  • 实施适当的查询失效策略,保持应用程序中数据的同步
  • 避免常见陷阱,如将 TanStack Query 用于本地状态或在查询键中忽略动态参数

手动数据获取的问题

这是一个典型的获取数据的 React 组件:

import { useState, useEffect } from 'react';

function UserList() {
  const [users, setUsers] = useState([]);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);

  useEffect(() => {
    let cancelled = false;
    
    async function fetchUsers() {
      setIsLoading(true);
      setError(null);
      
      try {
        const response = await fetch('/api/users');
        if (!response.ok) throw new Error('Failed to fetch');
        
        const data = await response.json();
        if (!cancelled) {
          setUsers(data);
        }
      } catch (err) {
        if (!cancelled) {
          setError(err.message);
        }
      } finally {
        if (!cancelled) {
          setIsLoading(false);
        }
      }
    }
    
    fetchUsers();
    
    return () => { cancelled = true };
  }, []);

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  
  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

这样做是可以的,但看看我们需要管理什么:

  • 三个独立的状态片段
  • 清理逻辑以防止在已卸载组件上更新状态
  • 没有缓存——每次挂载都会触发新的获取
  • 失败请求没有重试逻辑
  • 无法知道我们的数据是否过期

现在将这种复杂性乘以几十个组件。这就是 TanStack Query 的用武之地。

TanStack Query 如何让数据获取更智能

TanStack Query 将服务器状态视为一等公民。你无需手动编排获取操作,而是声明式地描述你的数据需求:

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

function UserList() {
  const { data: users, isLoading, error } = useQuery({
    queryKey: ['users'],
    queryFn: () => fetch('/api/users').then(res => {
      if (!res.ok) throw new Error('Failed to fetch');
      return res.json();
    }),
  });

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  
  return (
    <ul>
      {users?.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

功能相同,但注意缺少了什么:

  • 没有用于数据、加载或错误的 useState
  • 没有 useEffect 或清理逻辑
  • 没有手动状态同步

但你获得了什么:

  • 自动缓存:离开页面再返回,立即看到结果
  • 后台重新获取:过期数据静默更新
  • 请求去重:多个组件可以共享同一个查询
  • 内置重试逻辑:失败的请求自动重试
  • 乐观更新:UI 在服务器确认之前就更新

智能数据管理的核心概念

查询:读取服务器状态

查询获取并缓存数据。useQuery hook 接受一个配置对象,包含两个基本属性:

const { data, isLoading, error } = useQuery({
  queryKey: ['todos', userId, { status: 'active' }],
  queryFn: () => fetchTodosByUser(userId, { status: 'active' }),
  staleTime: 5 * 60 * 1000, // 5 分钟
  gcTime: 10 * 60 * 1000, // 10 分钟(原 cacheTime)
});

查询键唯一标识每个查询。它们是数组,可以包含:

  • 静态标识符:['users']
  • 动态参数:['user', userId]
  • 复杂过滤器:['todos', { status, page }]

当查询键的任何部分发生变化时,TanStack Query 就知道要获取新数据。

Mutations:更改服务器状态

查询读取数据,而 mutations 修改数据:

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

function CreateTodo() {
  const queryClient = useQueryClient();
  
  const mutation = useMutation({
    mutationFn: (newTodo) => 
      fetch('/api/todos', {
        method: 'POST',
        body: JSON.stringify(newTodo),
        headers: { 'Content-Type': 'application/json' },
      }).then(res => {
        if (!res.ok) throw new Error('Failed to create todo');
        return res.json();
      }),
    onSuccess: () => {
      // 失效并重新获取
      queryClient.invalidateQueries({ queryKey: ['todos'] });
    },
  });

  return (
    <button 
      onClick={() => mutation.mutate({ title: 'New Todo' })}
      disabled={mutation.isPending}
    >
      {mutation.isPending ? 'Creating...' : 'Create Todo'}
    </button>
  );
}

Mutations 处理完整的生命周期:加载状态、错误处理和成功回调。onSuccess 回调非常适合在更改后更新缓存。

查询失效:保持数据新鲜

失效将查询标记为过期,触发后台重新获取:

// 失效所有查询
queryClient.invalidateQueries();

// 失效特定查询
queryClient.invalidateQueries({ queryKey: ['todos'] });

// 精确匹配失效
queryClient.invalidateQueries({ 
  queryKey: ['todo', 5], 
  exact: true 
});

这就是 TanStack Query 在 mutations 后保持 UI 同步的方式。更改了一个 todo?失效 todos 列表。更新了用户?失效该用户的数据。

在 React 应用中设置 TanStack Query

首先,安装包:

npm install @tanstack/react-query

然后用 QueryClient provider 包装你的应用:

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

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 60 * 1000, // 1 分钟
      gcTime: 5 * 60 * 1000, // 5 分钟(原 cacheTime)
      retry: 3,
      retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),
    },
  },
});

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

QueryClient 管理所有缓存和同步。通常为整个应用创建一个实例。

生产应用的高级模式

乐观更新

立即更新 UI,然后与服务器同步:

const mutation = useMutation({
  mutationFn: updateTodo,
  onMutate: async (newTodo) => {
    // 取消正在进行的重新获取
    await queryClient.cancelQueries({ queryKey: ['todos'] });
    
    // 快照之前的值
    const previousTodos = queryClient.getQueryData(['todos']);
    
    // 乐观更新
    queryClient.setQueryData(['todos'], old => 
      old ? [...old, newTodo] : [newTodo]
    );
    
    // 返回回滚的上下文
    return { previousTodos };
  },
  onError: (err, newTodo, context) => {
    // 错误时回滚
    if (context?.previousTodos) {
      queryClient.setQueryData(['todos'], context.previousTodos);
    }
  },
  onSettled: () => {
    // 错误或成功后总是重新获取
    queryClient.invalidateQueries({ queryKey: ['todos'] });
  },
});

依赖查询

链接相互依赖的查询:

const { data: user } = useQuery({
  queryKey: ['user', email],
  queryFn: () => getUserByEmail(email),
});

const { data: projects } = useQuery({
  queryKey: ['projects', user?.id],
  queryFn: () => getProjectsByUser(user.id),
  enabled: !!user?.id, // 只有当用户 ID 存在时才运行
});

无限查询

使用无限滚动处理分页数据:

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

const {
  data,
  fetchNextPage,
  hasNextPage,
  isFetchingNextPage,
} = useInfiniteQuery({
  queryKey: ['projects'],
  queryFn: ({ pageParam = 0 }) => fetchProjects({ page: pageParam }),
  getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
  initialPageParam: 0,
});

常见陷阱和解决方案

忘记 QueryClientProvider

如果你的查询返回 undefined 或根本不工作,检查你的应用是否用 QueryClientProvider 包装。这是最常见的设置错误。

将 TanStack Query 用于本地状态

TanStack Query 是为服务器状态设计的。不要将其用于表单输入、UI 切换或其他仅客户端状态。对于这些情况,坚持使用 useStateuseReducer

动态数据使用静态查询键

始终在查询键中包含动态参数:

// ❌ 错误 - 所有用户将显示相同数据
useQuery({
  queryKey: ['user'],
  queryFn: () => fetchUser(userId),
});

// ✅ 正确 - 每个用户的唯一缓存条目
useQuery({
  queryKey: ['user', userId],
  queryFn: () => fetchUser(userId),
});

结论

TanStack Query 将 React 数据获取从手动、容易出错的过程转变为声明式、健壮的系统。通过将服务器状态视为与客户端状态根本不同的东西,它消除了样板代码,同时添加了缓存、同步和乐观更新等强大功能。

思维转变很简单:停止思考如何获取数据,开始思考需要什么数据。通过查询键和函数声明你的需求,然后让 TanStack Query 处理保持一切同步的复杂性。

从小处开始——用 useQuery 替换一个 useEffect 获取。一旦你看到即时加载状态、自动重试和无缝缓存的实际效果,你就会明白为什么 TanStack Query 已成为现代 React 应用程序的必需品。

常见问题

它们是同一个库。React Query 被重命名为 TanStack Query,以反映它现在支持 React 之外的多个框架。React 特定的包仍在维护,并使用你熟悉的相同 API。

可以,但对于服务器状态你可能不需要。TanStack Query 自动处理缓存和在组件之间共享数据。将 Redux 或 Context 用于真正的客户端状态,如用户偏好或 UI 状态,将 TanStack Query 用于来自 API 的任何内容。

默认情况下,查询在窗口聚焦、重新连接和挂载时重新获取。你可以通过 staleTime(数据保持新鲜的时间)和 gcTime(保留未使用数据的时间)控制新鲜度。过期查询在后台重新获取,同时立即显示缓存数据。

失效将查询标记为过期但不立即重新获取——它等待查询再次被使用。直接重新获取触发立即网络请求。在 mutations 后使用失效以获得更好的性能,因为它只重新获取实际正在显示的查询。

TanStack Query 通过轮询和重新获取间隔在近实时更新方面表现出色。对于真正的实时需求,将其与 WebSockets 结合:使用 socket 进行实时更新,使用 TanStack Query 进行初始数据加载和 socket 断开连接时的回退。

是的。TanStack Query 5+ 与 React 19 完全兼容。这里使用的 API(查询、mutations、失效)没有变化,所以本文中的示例无需修改即可工作。

Listen to your bugs 🧘, with OpenReplay

See how users use your app and resolve issues fast.
Loved by thousands of developers