12k
All articles

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

TanStack Query 为 React 应用提供缓存、重试与查询失效处理,以声明式数据获取方式替代手动状态逻辑。

OpenReplay Team
OpenReplay Team
在 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 应用程序的必需品。

常见问题

TanStack Query 和 React Query 有什么区别?

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

我可以将 TanStack Query 与 Redux 或 Context API 一起使用吗?

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

TanStack Query 如何知道何时重新获取数据?

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

查询失效和重新获取有什么区别?

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

TanStack Query 适合实时数据吗?

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

这与 React 19 兼容吗?

是的。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

We use cookies to improve your experience. By using our site, you accept cookies.