阅读 12 分钟

TanStack Query 的正确使用方法

为什么选择 TanStack Query

TanStack Query 的核心理念源自 Vercel 开发的 SWR 库,并在此基础上进行了优化和扩展。

The name "SWR" is derived from stale-while-revalidate, a HTTP cache invalidation strategy popularized by HTTP RFC 5861. SWR is a strategy to first return the data from cache (stale), then send the fetch request (revalidate), and finally come with the up-to-date data.

"SWR" 这个名字来源于 stale-while-revalidate——一种由 HTTP RFC 5861 推广的 HTTP 缓存失效策略。该策略首先从缓存中返回数据(可能已过期),同时发送请求进行重新验证,最终返回最新数据。

TanStack Query 通过实现类似的缓存与重新验证机制,帮助开发者更高效地管理服务器状态和数据获取。它提供了一套强大的工具和 API,让数据的获取、缓存、同步和更新变得简单高效。

核心 Hooks:useQuery 与 useMutation

TanStack Query 中最常用的两个 Hooks 是 useQueryuseMutation,分别用于数据获取和数据变更。

useQuery:数据获取

useQuery 用于获取数据,接受查询键(Query Key)和异步函数作为参数,返回查询状态和数据。

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

const fetchUser = async (userId) => {
  const { data } = await axios.get(`/api/users/${userId}`);
  return data;
};

const UserProfile = ({ userId }) => {
  const { data, error, isLoading } = useQuery({
    queryKey: ['users', userId],
    queryFn: () => fetchUser(userId),
  });

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error loading user</div>;

  return (
    <div>
      <h1>{data.name}</h1>
      <p>{data.email}</p>
    </div>
  );
};

useMutation:数据变更

useMutation 用于执行创建、更新或删除等数据变更操作,接受异步函数作为参数,返回变更状态和触发函数。

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

const createUser = async (newUser) => {
  const { data } = await axios.post('/api/users', newUser);
  return data;
};

const NewUserForm = () => {
  const queryClient = useQueryClient();
  const mutation = useMutation({
    mutationFn: createUser,
    onSuccess: () => {
      // 变更成功后,使用户列表缓存失效,触发重新获取
      queryClient.invalidateQueries({ queryKey: ['users'] });
    },
  });

  const handleSubmit = (event) => {
    event.preventDefault();
    const newUser = {
      name: event.target.name.value,
      email: event.target.email.value,
    };
    mutation.mutate(newUser);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input name="name" placeholder="Name" required />
      <input name="email" placeholder="Email" required />
      <button type="submit">Create User</button>
      {mutation.isLoading && <p>Creating user...</p>}
      {mutation.isError && <p>Error creating user</p>}
      {mutation.isSuccess && <p>User created successfully!</p>}
    </form>
  );
};

使用优势

相比直接调用 fetchUsercreateUser 函数,useQueryuseMutation 的优势在于:

  • 自动状态管理:内置 isLoadingisErrorisSuccess 等状态
  • 智能缓存:自动缓存数据,避免重复请求
  • 后台更新:支持窗口聚焦时自动刷新数据
  • 乐观更新:支持在请求完成前先更新 UI
⚠️ 注意:尽管这两个 Hooks 功能强大,但并非所有场景都需要使用。对于简单的一次性请求或复杂的自定义状态管理场景,可以结合其他工具灵活处理。

理解 Query Key

Query Key 是 TanStack Query 用于标识和缓存查询结果的唯一标识符。正确设计 Query Key 对于确保数据一致性和缓存有效性至关重要。

Query Key 设计原则

Query Key 应该是唯一且具有描述性的标识符,通常使用数组形式。数组第一个元素代表资源名称,后续元素为查询相关参数。

// 推荐:使用数组形式,清晰表达资源层级关系
const { data } = useQuery({
  queryKey: ['users', userId],
  queryFn: () => fetchUser(userId),
});

在 Vue.js 中,因为 Vue 使用响应式系统来处理追踪和变动。一般来说组件内的状态都会通过 refreactive 来定义,当这些值传进 queryKey 数组时,我们应该传入 Ref<T> 或者 Reactive<T>,而不是 ref.value 。详情请参考:Reactivity | TanStack Query Vue Docs

export function useUserProjects(userId: Ref<string>) {
  return useQuery(
    queryKey: ['userProjects', userId],
    queryFn: () => api.fetchUserProjects(userId.value),
  );
}

缓存失效与数据同步

通过 invalidateQueries 方法可以使相关查询缓存失效,触发数据重新获取:

const queryClient = useQueryClient();

const mutation = useMutation({
  mutationFn: createUser,
  onSuccess: () => {
    // 使所有以 'users' 开头的查询缓存失效
    queryClient.invalidateQueries({ queryKey: ['users'] });
  },
});

有时候更新数据的接口会返回更新成功后的数据,这个时候没必要刷新(Invalidate)整个列表,可以直接将更新数据接口返回的数据设置到列表查询的数据中:

// The query below will be updated with the response from the
// successful mutation
const { status, data, error } = useQuery({
  queryKey: ['todos', { id: 5 }],
  queryFn: fetchTodoById,
})

const mutation = useMutation({
  mutationFn: editTodo,
  onSuccess: (data) => {
    queryClient.setQueryData(['todos', { id: 5 }], data)
  },
})

mutation.mutate({
  id: 5,
  name: 'Do the laundry',
})

乐观更新(Optimistic Updates)

乐观更新是一种提升用户体验的技术:在服务器响应前先更新本地缓存,让用户立即看到变化。

const queryClient = useQueryClient();

const mutation = useMutation({
  mutationFn: updateUser,
  onMutate: async (updatedUser) => {
    // 取消正在进行的查询,避免覆盖乐观更新
    await queryClient.cancelQueries({ queryKey: ['users', updatedUser.id] });

    // 保存当前数据用于回滚
    const previousUserData = queryClient.getQueryData(['users', updatedUser.id]);

    // 乐观更新缓存
    queryClient.setQueryData(['users', updatedUser.id], updatedUser);

    return { previousUserData };
  },
  onError: (err, updatedUser, context) => {
    // 发生错误时回滚到之前的数据
    queryClient.setQueryData(['users', updatedUser.id], context.previousUserData);
  },
  onSettled: () => {
    // 无论成功失败,最终都重新获取最新数据
    queryClient.invalidateQueries({ queryKey: ['users'] });
  },
});

封装自定义 Query Hook

为提高代码可复用性和可维护性,建议将每个 useQuery 封装为自定义 Hook:

// api/user.ts - API 层
export const fetchUser = async (userId: string) => {
  const { data } = await axios.get(`/api/users/${userId}`);
  return data;
};
// hooks/useUserQuery.ts - Query Hook 层
import { useQuery } from '@tanstack/react-query';
import { fetchUser } from '../api/user';

// Query Key 工厂函数
export const getUserQueryKey = (userId: string) => ['users', userId];

// 自定义 Query Hook
export const useUserQuery = (userId: string) => {
  return useQuery({
    queryKey: getUserQueryKey(userId),
    queryFn: () => fetchUser(userId),
  });
};

// 即使无参数,也使用函数封装 Query Key,保持一致性
export const getMyProfileQueryKey = () => ['myProfile'];

export const useMyProfileQuery = () => {
  return useQuery({
    queryKey: getMyProfileQueryKey(),
    queryFn: fetchMyProfile,
  });
};


export const useMutationUpdateMyProfile = (docs: any) => {
	return useMutation({
		mutationFn: () => { /* 更新用户信息 */ },
		onSuccess: () => {
			// 成功将新的用户信息更新到后端之后,将前端的相关数据标记为过期,tanstack query 会自动在合适的时机重新请求数据
			queryClient.invalidateQueries({ queryKey: getMyProfileQueryKey() })
		}
	})
}

function DemoPage() {
	const { data: profile, refetch: refetchProfile } = useMyProfileQuery()
	const updateProfileMutation = useMutationUpdateMyProfile()
	
	const onButtonClick = () => {
		// ✅ 只需要写这一行
		updateProfileMutation.mutate()
		
		// ‼️ 不需要手动重新请求数据
		await updateProfileMutation.mutate()
		await refetchProfile()
	}

}

命名规范:

  • Hook 命名:useXxxQueryuseXxxMutation
  • Query Key 工厂:getXxxQueryKey
  • 始终返回 useQuery 的完整返回值,不要解构后再返回

封装获取 Query Options 的函数

相比于每个请求封装一个 Hooks,也可以为每一个请求创建一个函数方法,用来返回 Query Options。

import { queryOptions, infiniteQueryOptions } from '@tanstack/react-query'

function queryUserOptions(userId: number) {
  // 使用 queryOptions 来创建,`queryUserOptions` 函数可以自动推断出返回类型
  return queryOptions({
    queryKey: ['users', userId],
    queryFn: () => fetchUser(userId),
    staleTime: 5 * 1000,
  })
}

// 对于 Infinity Query 有一个特殊的工具函数
function infiniteQueryUserOptions(userId: number) {
  // 使用 queryOptions 来创建,`queryUserOptions` 函数可以自动推断出返回类型
  return infiniteQueryOptions({
    // ...
  })
}


useQuery(queryUserOptions(1))
useQueries({
  queries: [
    queryUserOptions(1), 
    queryUserOptions(2),
  ],
})
queryClient.prefetchQuery(queryUserOptions(23))
queryClient.setQueryData(queryUserOptions(42).queryKey, newGroups)

理解本质:状态管理工具而非请求库

TanStack Query 不是网络请求库,而是服务器状态管理工具。它不负责实际的网络请求,而是管理数据的获取、缓存和同步状态。实际请求仍需使用 axiosfetch 等库。

更进一步,queryFnmutationFn 中并不限于网络请求,你也可以执行:

  • IndexedDB / LocalStorage 操作
  • WebSocket 消息处理
  • 任何返回 Promise 的异步操作

最佳实践:API 方法应封装在独立模块中作为 SDK 使用,而非直接定义在 queryFnmutationFn 中。这样可以提高代码的可维护性和可测试性。

理解 staleTime 与 gcTime

这两个配置项是 TanStack Query 缓存机制的核心,容易混淆。

概念对比

配置项

含义

默认值

作用

staleTime

数据新鲜时间

0

数据在多长时间内被认为是「新鲜」的,不会触发后台重新获取

gcTime

垃圾回收时间

5 分钟

数据在缓存中保留多久后被清理(当没有组件订阅时开始计时)

生命周期图解

查询成功 ──────────────────────────────────────────────────────► 时间
    │
    ├── staleTime ──┤ 数据新鲜,不会后台刷新
    │               │
    │               ├── 数据过期(stale),后台刷新但仍返回缓存
    │               │
    └───────────────────── gcTime ─────────────────────────────┤
                                                               │
                                    组件卸载后开始计时,到期后清理缓存

使用示例

const { data } = useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,
  staleTime: 1000 * 60 * 5,  // 5 分钟内数据保持新鲜
  gcTime: 1000 * 60 * 30,    // 缓存保留 30 分钟
});

常见场景配置

// 场景 1:几乎不变的数据(如配置项)
{
  staleTime: Infinity,  // 永不过期,除非手动 invalidate
  gcTime: Infinity,
}

// 场景 2:频繁变化的数据(如实时消息)
{
  staleTime: 0,         // 每次都后台刷新(默认值)
  gcTime: 1000 * 60,    // 缓存 1 分钟
}

// 场景 3:用户信息(偶尔变化)
{
  staleTime: 1000 * 60 * 10,  // 10 分钟内认为新鲜
  gcTime: 1000 * 60 * 60,     // 缓存 1 小时
}

全局默认配置

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60,      // 全局默认 1 分钟新鲜期
      gcTime: 1000 * 60 * 10,    // 全局默认缓存 10 分钟
    },
  },
});
💡 提示staleTime 应始终小于或等于 gcTime。如果 staleTime > gcTime,数据可能在「新鲜期」内就被清理掉了。

区分 isPending、isLoading、isFetching

正确理解这三个状态是使用 TanStack Query 的关键。

状态定义

状态

定义

触发条件

isPending

data === undefined

没有任何缓存数据(包括 enabled: false 导致未请求的情况)

isLoading

isPending && isFetching

首次加载:无缓存且正在请求

isFetching

请求进行中

任何请求中,包括后台刷新

使用场景

const { data, isPending, isLoading, isFetching } = useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,
});

// ❌ 不推荐:使用 isPending 会在 enabled: false 时也显示加载状态
if (isPending) return <Skeleton />;

// ✅ 推荐:首次加载使用 isLoading
if (isLoading) return <FullPageSpinner />;

// ✅ 推荐:后台刷新使用 isFetching
return (
  <div>
    <Header>
      用户列表 {isFetching && <SmallSpinner />}
    </Header>
    <UserList users={data} />
  </div>
);

选择指南

  • isLoading:首次加载页面时显示全屏加载状态
  • isFetching:已有数据时显示轻量级刷新指示器
  • isPending:仅关心是否有数据可用时使用

持久化缓存(Persister)

TanStack Query 支持将查询结果持久化到本地存储(LocalStorage、IndexedDB 等),实现页面刷新后快速恢复数据,显著提升用户体验。

gcTime 与持久化的关系

理解 gcTime 与 Persister 的配合至关重要:

页面关闭 ──► 数据写入 LocalStorage ──► 页面重新打开 ──► 从 LocalStorage 恢复数据
                                                              │
                                                              ▼
                                              gcTime 检查:数据是否已过期?
                                                    │
                                    ┌───────────────┴───────────────┐
                                    ▼                               ▼
                              未过期:使用缓存                   已过期:丢弃缓存
                              (可能触发后台刷新)              (重新请求数据)

关键点:持久化存储的数据在恢复时,会检查其是否超过 gcTime。如果超过,数据会被丢弃而非恢复。

// ❌ 错误示例:gcTime 太短,持久化几乎无意义
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      gcTime: 1000 * 60 * 5,  // 5 分钟后数据就会被丢弃
    },
  },
});

// ✅ 正确示例:持久化场景需要较长的 gcTime
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      gcTime: 1000 * 60 * 60 * 24,  // 24 小时
    },
  },
});
⚠️ 重要:如果你希望用户第二天打开应用时仍能看到昨天的缓存数据,gcTime 至少要设置为 24 小时以上。

基础配置

npm install @tanstack/react-query-persist-client
import { QueryClient } from '@tanstack/react-query';
import { PersistQueryClientProvider } 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 小时
    },
  },
});

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

function App() {
  return (
    <PersistQueryClientProvider
      client={queryClient}
      persistOptions={{ persister }}
    >
      <YourApp />
    </PersistQueryClientProvider>
  );
}

注意事项

要点

说明

gcTime

需设置足够长,否则数据可能在恢复前被清理

序列化

仅支持 JSON 可序列化数据,DateMapSet 需特殊处理

容量限制

LocalStorage 约 5MB,大数据量建议使用 IndexedDB

版本管理

数据结构变更时,通过 buster 选项清理旧缓存

按需持久化

通常只有部分查询需要持久化。通过 meta 属性可以精细控制:

// 标记不需要持久化的查询
const { data } = useQuery({
  queryKey: ['sensitiveData'],
  queryFn: fetchSensitiveData,
  meta: { persist: false },
});
// 配置 Persister 过滤逻辑
<PersistQueryClientProvider
  client={queryClient}
  persistOptions={{
    persister,
    dehydrateOptions: {
      shouldDehydrateQuery: (query) => {
        return (
          query.state.status === 'success' &&
          query.meta?.persist !== false
        );
      },
    },
  }}
>
  <App />
</PersistQueryClientProvider>

小程序适配

在小程序(微信、支付宝等)中使用 TanStack Query 需要处理以下兼容性问题:

问题

原因

解决方案

AbortController

小程序不支持原生 AbortController

引入 polyfill

焦点检测

visibilitychange 事件

使用 Taro/Uniapp 生命周期

网络状态

online/offline 事件

使用 Taro/Uniapp API

Taro 适配示例

import { PropsWithChildren, useEffect } from 'react';
import {
  focusManager,
  onlineManager,
  QueryClient,
  QueryClientProvider,
} from '@tanstack/react-query';
import Taro, { useDidHide, useDidShow } from '@tarojs/taro';
import 'abortcontroller-polyfill/dist/abortcontroller-polyfill-only';

const queryClient = new QueryClient();

function ReactQueryWrapper({ children }: PropsWithChildren) {
  // 焦点管理:页面显示/隐藏
  useDidShow(() => focusManager.setFocused(true));
  useDidHide(() => focusManager.setFocused(false));

  // 网络状态管理
  useEffect(() => {
    // 初始化网络状态
    Taro.getNetworkType({
      success: ({ networkType }) => {
        onlineManager.setOnline(networkType !== 'none');
      },
    });

    // 监听网络变化
    const handleNetworkChange: Taro.onNetworkStatusChange.Callback = ({
      isConnected,
      networkType,
    }) => {
      onlineManager.setOnline(isConnected ?? networkType !== 'none');
    };

    Taro.onNetworkStatusChange(handleNetworkChange);

    return () => {
      Taro.offNetworkStatusChange(handleNetworkChange);
    };
  }, []);

  return (
    <QueryClientProvider client={queryClient}>
      {children}
    </QueryClientProvider>
  );
}

export default ReactQueryWrapper;
💡 提示:可以使用条件编译,仅在小程序端应用这些适配逻辑,Web 端保留原有行为。