TanStack Query 原理剖析:以缓存失效策略替代传统全局 Store
各位同仁,下午好。今天,我们将深入探讨一个在现代前端开发中日益重要的库——TanStack Query(前身为 React Query)。它在数据管理领域,尤其是在处理服务器状态时,提出了一种与传统全局状态管理库截然不同的范式。我们将聚焦其核心原理:如何通过精妙的缓存失效策略,优雅地替代了我们过去为管理服务器数据而构建的复杂全局 Store 体系。
传统全局 Store 在服务器状态管理中的困境
在深入 TanStack Query 之前,我们有必要回顾一下传统的全局状态管理方案,例如 Redux、MobX 或 Zustand,它们在处理客户端状态方面的强大能力是毋庸置疑的。它们提供了一个单一的、可预测的状态树,使得状态的更新和派发变得清晰。然而,当我们将这些工具应用于管理“服务器状态”时,问题便层出不穷。
服务器状态(Server State)与客户端状态(Client State)有着本质的区别:
- 异步性: 服务器状态的数据总是异步获取的。
- 共享性: 多个组件可能依赖同一份服务器数据。
- 可变性: 服务器数据可能在任何时候被其他用户或系统更改。
- 缓存需求: 为了性能和用户体验,我们通常需要缓存服务器数据。
- 数据同步: 客户端缓存的数据需要与服务器保持同步。
传统的全局 Store,在面对服务器状态时,通常需要开发者手动处理以下繁琐而易错的环节:
- 加载状态管理: 追踪数据是否正在加载 (
isLoading)、是否加载成功 (isSuccess)、是否加载失败 (isError)。 - 错误处理: 捕获并展示请求过程中发生的错误。
- 数据缓存: 决定何时缓存数据、缓存多久,以及何时清除缓存。
- 数据去重: 避免在短时间内重复请求同一份数据。
- 数据同步: 当服务器数据发生变化时(例如,通过
POST/PUT/DELETE请求),如何使客户端缓存的数据保持最新。这通常需要手动触发重新获取或复杂的数据更新逻辑。 - 竞争条件: 多个请求同时发出,哪个请求的结果应该被采纳?
- 内存管理: 长期不用的数据何时从 Store 中移除以节省内存。
为了解决这些问题,我们往往需要在 Redux 中编写大量的 reducer、action、saga 或 thunk,并手动构建复杂的缓存逻辑。这不仅增加了大量的样板代码,也极大地提升了心智负担。例如,一个简单的用户列表获取,可能需要定义 FETCH_USERS_REQUEST, FETCH_USERS_SUCCESS, FETCH_USERS_FAILURE 三种 action 类型,以及对应的 reducer 逻辑来更新 isLoading、data、error 状态。当涉及到数据更新后的缓存失效,复杂度更是成倍增长。
TanStack Query 的核心哲学:拥抱服务器状态的特性
TanStack Query 的诞生,正是为了解决传统全局 Store 在管理服务器状态时的痛点。它的核心哲学是:将服务器状态视为一等公民,并提供一套开箱即用的、声明式的数据获取、缓存和同步机制。 它不是要替代 Redux 或 Zustand 来管理所有的应用程序状态,而是专注于服务器状态的管理,让开发者能够将精力集中在业务逻辑上,而不是繁琐的数据管理细节。
TanStack Query 的设计理念基于以下几个关键原则:
- 约定优于配置: 提供合理的默认行为,减少配置工作。
- 声明式 API: 通过 Hooks 提供直观的 API,描述你想要什么数据,而不是如何获取数据。
- 缓存优先: 大部分操作都围绕着缓存进行,提供即时响应。
- “Stale-While-Revalidate” (SWR) 策略: 先返回旧数据,同时在后台重新验证,确保数据最终的一致性。
- 自动管理: 自动处理加载、错误状态、数据去重、垃圾回收等。
核心原理:Query Cache 与 Stale-While-Revalidate
TanStack Query 的核心在于其内部的 Query Cache 和基于该缓存的 Stale-While-Revalidate (SWR) 策略。
1. Query Cache:服务器状态的内存存储
TanStack Query 在内部维护了一个全局的 Query Cache。这个缓存是一个键值对存储,其中:
- 键 (Key): 称之为
queryKey,它是一个唯一的标识符,用于识别和检索特定的服务器数据。queryKey是一个数组,可以包含字符串和任意可序列化的对象,这使得我们可以非常灵活地定义查询的粒度。例如,['todos']可以表示所有待办事项,而['todos', { status: 'active' }]则表示所有活跃的待办事项。 - 值 (Value): 存储着与该
queryKey关联的服务器数据,以及该数据的元信息,如加载状态、错误信息、上次更新时间、是否过期等。
当一个组件通过 useQuery Hook 请求数据时,TanStack Query 会首先检查 Query Cache。如果缓存中存在该 queryKey 对应的数据,并且该数据被认为是“新鲜的”,那么它会立即返回缓存中的数据。否则,它会触发数据获取。
Query Cache 的结构(概念性):
| 属性 | 描述 | 示例值 |
|---|---|---|
queryKey |
唯一的查询标识符,一个数组。 | ['posts', 1] |
queryFn |
实际执行数据获取的异步函数。 | () => fetch('/api/posts/1').then(res => res.json()) |
state.data |
获取到的数据。 | { id: 1, title: 'My First Post', content: '...' } |
state.status |
查询的当前状态(loading, success, error)。 |
success |
state.error |
如果查询失败,存储错误信息。 | null 或 { message: 'Network error' } |
state.dataUpdatedAt |
数据最后一次成功更新的时间戳。 | 1678886400000 |
state.isFetching |
是否正在后台重新获取数据。 | false |
state.isStale |
数据是否已过期(需要重新验证)。 | false |
observers |
订阅该 Query 的组件列表(用于管理组件的重新渲染)。 | [ComponentA, ComponentB] |
cacheTime |
数据在不被观察时,在缓存中保留多长时间(默认为 5 分钟)。 | 300000 (5 minutes) |
staleTime |
数据在被认为是“新鲜的”状态下保留多长时间(默认为 0)。 | 0 |
2. Stale-While-Revalidate (SWR) 策略:速度与新鲜度的平衡
SWR 是一种非常强大的缓存策略,它解决了传统缓存面临的“要么快但可能旧,要么新但可能慢”的困境。TanStack Query 默认就实现了 SWR 模式。
其核心思想是:
- Stale (过期): 当数据在缓存中存在,但其
staleTime已经过去时,数据就被认为是“过期”的。 - Revalidate (重新验证): 当数据过期,并且有组件正在使用它时,TanStack Query 会在后台静默地重新获取数据。
- While (同时): 在后台重新验证数据期间,它会立即向 UI 返回缓存中已有的“过期”数据。
这带来了巨大的用户体验提升:
- 即时响应: 用户不会因为等待数据加载而看到空白或加载 Spinners,而是立即看到之前的数据。
- 数据最终一致性: 一旦后台重新验证成功,UI 会自动更新为最新数据。
让我们通过一个简单的流程图来理解 SWR:
- 组件渲染并请求数据 (
useQuery) - 检查 Query Cache:
- 命中且数据新鲜 (
staleTime未到期):- 立即返回缓存数据。
- UI 渲染。
- 结束。
- 未命中或数据过期 (
staleTime已到期):- 如果缓存中有旧数据,立即返回旧数据并标记为
isFetching: true。 - 同时 在后台启动
queryFn重新获取数据。 - UI 渲染旧数据。
- 如果缓存中有旧数据,立即返回旧数据并标记为
- 命中且数据新鲜 (
- 后台数据获取完成:
- 成功:
- 更新 Query Cache 中的数据。
- 将数据标记为
isFetching: false,isStale: false。 - 通知所有观察者 (Observer) 数据已更新,触发组件重新渲染最新数据。
- 失败:
- 更新 Query Cache 中的错误信息。
- 将数据标记为
isFetching: false。 - 通知所有观察者数据获取失败,触发组件重新渲染错误状态。
- 成功:
TanStack Query 的缓存失效策略与机制
现在我们来详细探讨 TanStack Query 如何通过各种缓存失效策略,替代传统的全局 Store 中手动的数据同步逻辑。
1. 查询的生命周期与状态
理解 TanStack Query 的查询状态至关重要。一个查询可以处于以下几种核心状态:
| 状态类型 | 描述 | Hook 属性 |
|---|---|---|
loading |
查询正在进行中,且没有旧数据。 | isLoading (或 status === 'loading') |
success |
查询成功,并有数据可用。 | isSuccess (或 status === 'success') |
error |
查询失败。 | isError (或 status === 'error') |
isFetching |
查询正在后台重新获取数据(可能已经有数据)。 | isFetching |
isStale |
缓存中的数据已过期,需要重新验证(当 staleTime 过去后)。 |
内部状态,影响重新获取逻辑 |
isInactive |
没有组件订阅该查询。数据会在 cacheTime 后被垃圾回收。 |
内部状态,影响垃圾回收 |
这些状态是自动管理的,开发者只需通过 useQuery Hook 提供的返回值来使用它们,无需手动设置。
import { useQuery } from '@tanstack/react-query';
interface Post {
id: number;
title: string;
body: string;
}
async function fetchPost(id: number): Promise<Post> {
const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
}
function PostDetail({ postId }: { postId: number }) {
const { data, isLoading, isError, error, isFetching } = useQuery<Post, Error>({
queryKey: ['post', postId], // 唯一的查询键
queryFn: () => fetchPost(postId), // 数据获取函数
staleTime: 5 * 60 * 1000, // 数据在 5 分钟内被认为是新鲜的
});
if (isLoading) return <div>Loading post...</div>;
if (isError) return <div>Error: {error?.message}</div>;
return (
<div>
<h2>{data?.title} {isFetching ? ' (Updating...)' : ''}</h2>
<p>{data?.body}</p>
</div>
);
}
在这个例子中:
queryKey: ['post', postId]唯一标识了这篇博文。queryFn定义了如何获取数据。staleTime: 5 * 60 * 1000意味着数据在缓存中 5 分钟内是新鲜的。如果在 5 分钟内再次渲染PostDetail组件,它会直接使用缓存数据,不会重新请求。5 分钟后,数据变为stale,下次组件挂载、窗口聚焦等事件发生时,会触发后台重新获取。isLoading仅在首次加载时为true。isFetching在首次加载和后台重新获取时都为true。
2. 自动重新获取 (Automatic Refetching)
TanStack Query 提供了多种自动重新获取数据的策略,以确保数据尽可能保持最新:
refetchOnWindowFocus(默认true): 当浏览器窗口重新获得焦点时,所有stale的查询都会在后台重新获取。这对于保持数据新鲜度非常有用,例如用户切换到其他标签页,回来后数据自动更新。refetchOnReconnect(默认true): 当检测到网络重新连接时,所有stale的查询都会在后台重新获取。这解决了离线后数据同步的问题。refetchOnMount(默认true): 当组件挂载时,如果数据是stale的,就会触发后台重新获取。staleTime(默认0): 这是控制数据新鲜度的关键参数。- 如果
staleTime为0(默认值),数据在获取成功后立即被标记为stale。这意味着每次组件挂载或窗口聚焦时,即使缓存中有数据,也会在后台重新获取。 - 如果
staleTime设置为Infinity,数据将永远不会被标记为stale,除非手动失效。这适用于那些几乎不变化的静态数据。
- 如果
// 在 QueryClientProvider 中配置全局默认值
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false, // 关闭窗口聚焦时自动重新获取
staleTime: 1000 * 60 * 5, // 全局设置数据 5 分钟新鲜
},
},
});
function App() {
return (
<QueryClientProvider client={queryClient}>
{/* ... 你的应用组件 */}
</QueryClientProvider>
);
}
3. 手动缓存失效 (Manual Cache Invalidation)
自动重新获取是基础,但最强大的缓存失效机制是手动触发。这通常发生在数据通过 POST、PUT、DELETE 等操作在服务器端发生改变之后。TanStack Query 提供了 queryClient.invalidateQueries 方法来实现这一点。
invalidateQueries 的作用是:
- 标记查询为
stale: 它会找到所有匹配queryKey的查询,并将它们的状态标记为stale。 - 触发重新获取: 对于当前正在被组件“观察”(即处于活跃状态)的
stale查询,它会立即在后台重新获取数据。对于不活跃的查询,它只会标记为stale,待下次被观察时再重新获取。
queryClient.invalidateQueries(queryKey, options)
queryKey: 可以是完整的queryKey,也可以是部分queryKey来匹配多个查询。['todos']: 使所有todos相关的查询失效。['todos', { status: 'active' }]: 使特定状态的todos查询失效。['todos', { status: 'active' }, 1]: 使特定状态和 ID 的todos查询失效。['todos', { status: 'active' }]匹配['todos', { status: 'active' }, 1]和['todos', { status: 'active' }, 2]。
options.refetchType:'active'(默认): 只重新获取当前被组件使用的查询。'inactive': 只重新获取当前不活跃的查询。'all': 重新获取所有匹配的查询(无论是否活跃)。
options.exact: 如果设置为true,则只匹配精确的queryKey。
示例:创建新待办事项后使列表失效
import { useMutation, useQueryClient } from '@tanstack/react-query';
interface Todo {
id: number;
title: string;
completed: boolean;
}
async function addTodo(newTodo: Omit<Todo, 'id'>): Promise<Todo> {
const response = await fetch('https://jsonplaceholder.typicode.com/todos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newTodo),
});
if (!response.ok) throw new Error('Failed to add todo');
return response.json();
}
function TodoCreator() {
const queryClient = useQueryClient(); // 获取 QueryClient 实例
const mutation = useMutation<Todo, Error, Omit<Todo, 'id'>>({
mutationFn: addTodo,
onSuccess: () => {
// 成功添加待办事项后,使所有 'todos' 相关的查询失效
// 这将触发所有使用 ['todos'] 或 ['todos', ...] 的组件重新获取数据
queryClient.invalidateQueries({ queryKey: ['todos'] });
console.log('Todo added successfully, invalidating todos queries.');
},
onError: (error) => {
console.error('Failed to add todo:', error.message);
},
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const formData = new FormData(e.currentTarget as HTMLFormElement);
const title = formData.get('title') as string;
if (title) {
mutation.mutate({ title, completed: false });
}
};
return (
<form onSubmit={handleSubmit}>
<input type="text" name="title" placeholder="New todo title" disabled={mutation.isLoading} />
<button type="submit" disabled={mutation.isLoading}>
{mutation.isLoading ? 'Adding...' : 'Add Todo'}
</button>
{mutation.isError && <div>Error: {mutation.error?.message}</div>}
</form>
);
}
// 假设我们有一个 TodoList 组件,它使用 useQuery(['todos']) 来获取待办事项列表
function TodoList() {
const { data, isLoading, isError, error } = useQuery<Todo[], Error>({
queryKey: ['todos'],
queryFn: async () => {
const response = await fetch('https://jsonplaceholder.typicode.com/todos?_limit=5');
if (!response.ok) throw new Error('Failed to fetch todos');
return response.json();
},
});
if (isLoading) return <div>Loading todos...</div>;
if (isError) return <div>Error: {error?.message}</div>;
return (
<ul>
{data?.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
);
}
// 在父组件中组合
function AppWithTodos() {
return (
<div>
<h1>My Todos</h1>
<TodoCreator />
<TodoList />
</div>
);
}
在这个 TodoCreator 示例中,当一个新的待办事项成功创建并发送到服务器后,我们调用 queryClient.invalidateQueries({ queryKey: ['todos'] })。这将告诉 TanStack Query:['todos'] 相关的缓存数据可能已经过时了。如果 TodoList 组件当前正在渲染并使用 ['todos'] 这个查询,TanStack Query 会自动在后台重新获取最新的待办事项列表,并更新 UI。这种方式极大地简化了数据同步的逻辑。
4. 直接更新缓存 (queryClient.setQueryData)
除了失效并重新获取,TanStack Query 还允许我们直接修改 Query Cache 中的数据。这在以下场景非常有用:
- 乐观更新 (Optimistic Updates): 在向服务器发送数据修改请求之前,立即更新 UI 以提供即时反馈。如果服务器请求失败,再回滚 UI。
- 客户端数据转换: 在不重新获取数据的情况下,根据某些客户端逻辑修改已有的缓存数据。
queryClient.setQueryData(queryKey, updater, options)
queryKey: 目标查询的键。updater: 可以是新数据,也可以是一个函数,该函数接收当前数据作为参数,并返回新数据。options.updatedAt: 可选,用于设置dataUpdatedAt,影响staleTime的计算。
示例:乐观更新待办事项的完成状态
import { useMutation, useQueryClient } from '@tanstack/react-query';
interface Todo {
id: number;
title: string;
completed: boolean;
}
async function updateTodoStatus(todoId: number, completed: boolean): Promise<Todo> {
const response = await fetch(`https://jsonplaceholder.typicode.com/todos/${todoId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ completed }),
});
if (!response.ok) throw new Error('Failed to update todo status');
return response.json();
}
function TodoItem({ todo }: { todo: Todo }) {
const queryClient = useQueryClient();
const mutation = useMutation<Todo, Error, { id: number; completed: boolean }>({
mutationFn: ({ id, completed }) => updateTodoStatus(id, completed),
// onMutate 在 mutationFn 运行之前触发,用于实现乐观更新
onMutate: async newTodoStatus => {
// 1. 取消任何正在进行的 ['todos'] 查询,避免乐观更新被覆盖
await queryClient.cancelQueries({ queryKey: ['todos'] });
// 2. 获取当前的 todos 列表快照,以便在出错时回滚
const previousTodos = queryClient.getQueryData<Todo[]>(['todos']);
// 3. 乐观更新缓存中的 todos 列表
queryClient.setQueryData<Todo[]>(['todos'], old =>
old?.map(t => (t.id === newTodoStatus.id ? { ...t, completed: newTodoStatus.completed } : t))
);
// 返回快照,以便 onError 或 onSettled 可以访问它
return { previousTodos };
},
onError: (err, newTodoStatus, context) => {
// 如果 mutation 失败,回滚到之前的状态
console.error('Optimistic update failed, rolling back:', err);
if (context?.previousTodos) {
queryClient.setQueryData<Todo[]>(['todos'], context.previousTodos);
}
},
onSettled: (data, error, newTodoStatus) => {
// 无论成功或失败,都在 mutation 结束后使 todos 列表查询失效
// 这会触发后台重新获取,确保数据与服务器最终一致
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
const toggleCompleted = () => {
mutation.mutate({ id: todo.id, completed: !todo.completed });
};
return (
<li>
<input
type="checkbox"
checked={todo.completed}
onChange={toggleCompleted}
disabled={mutation.isLoading}
/>
<span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
{todo.title}
</span>
{mutation.isLoading && ' (Updating...)'}
</li>
);
}
// TodoList 和 AppWithTodos 保持不变,TodoList 会渲染 TodoItem
function TodoListWithOptimisticUpdate() {
const { data, isLoading, isError, error } = useQuery<Todo[], Error>({
queryKey: ['todos'],
queryFn: async () => {
const response = await fetch('https://jsonplaceholder.typicode.com/todos?_limit=5');
if (!response.ok) throw new Error('Failed to fetch todos');
return response.json();
},
});
if (isLoading) return <div>Loading todos...</div>;
if (isError) return <div>Error: {error?.message}</div>;
return (
<ul>
{data?.map(todo => (
<TodoItem key={todo.id} todo={todo} />
))}
</ul>
);
}
// 在父组件中组合
function AppWithOptimisticTodos() {
return (
<div>
<h1>My Todos (Optimistic Update)</h1>
<TodoListWithOptimisticUpdate />
</div>
);
}
这个例子完美展示了 setQueryData 和 invalidateQueries 的协同作用。用户点击复选框时,UI 立即更新(乐观更新),然后发送请求。无论请求成功还是失败,onSettled 都会使 ['todos'] 查询失效,确保最终的数据一致性。如果请求失败,onError 会利用 onMutate 返回的 previousTodos 快照将 UI 状态回滚。
5. 移除缓存 (queryClient.removeQueries)
有时我们可能希望完全清除某个查询在缓存中的数据。这可以通过 queryClient.removeQueries 来实现。例如,用户登出时,我们可能需要清除所有与用户相关的敏感数据。
import { useQueryClient } from '@tanstack/react-query';
function LogoutButton() {
const queryClient = useQueryClient();
const handleLogout = () => {
// 执行登出逻辑...
// 移除所有与用户或认证相关的查询数据
queryClient.removeQueries({ queryKey: ['user'] });
queryClient.removeQueries({ queryKey: ['profile'] });
queryClient.removeQueries({ queryKey: ['settings'] });
// 也可以移除所有查询数据,但要小心,这会清空整个缓存
// queryClient.clear();
console.log('User logged out and cache cleared for user-related data.');
};
return (
<button onClick={handleLogout}>Logout</button>
);
}
TanStack Query 如何替代传统全局 Store?
通过上述的缓存管理和失效策略,我们可以清晰地看到 TanStack Query 如何在服务器状态管理方面超越并替代了传统全局 Store:
| 特性 | 传统全局 Store (如 Redux) | TanStack Query (针对服务器状态) |
|---|---|---|
| 数据源 | 主要管理客户端状态,服务器数据需手动集成。 | 专注于服务器状态,将服务器视为唯一真实数据源。 |
| Loading/Error | 需手动定义 action/reducer 及其状态。 | 内置 isLoading, isError, isFetching 等状态,自动管理。 |
| 缓存 | 需手动实现复杂的缓存逻辑。 | 内置 Query Cache,自动管理数据存储、垃圾回收。 |
| 数据去重 | 需手动防抖或节流请求。 | 自动去重相同 queryKey 的并发请求。 |
| 数据新鲜度 | 需手动触发重新获取。 | 基于 staleTime 和自动重新获取策略 (窗口聚焦、网络重连、组件挂载)。 |
| 数据同步 | POST/PUT/DELETE 后需手动重新获取或更新 Store。 |
invalidateQueries 自动标记数据过期并触发重新获取;setQueryData 乐观更新。 |
| 样板代码 | 大量 action、reducer、selector、middleware。 | 极简的 useQuery, useMutation hooks。 |
| 心智负担 | 管理异步流、缓存、竞争条件复杂。 | 声明式 API,抽象了大部分异步和缓存细节。 |
| UI 响应 | 通常等待数据完全加载后才更新 UI。 | SWR 策略,即时显示旧数据,后台更新,提升用户体验。 |
| 垃圾回收 | 需手动实现不活跃数据的清理。 | 基于 cacheTime 自动清理不活跃的查询。 |
TanStack Query 提供了一种“数据即服务”的理念,将服务器数据的生命周期管理、同步和缓存抽象化,让开发者无需再手动编写大量与数据获取和管理相关的代码。你的组件只需声明它需要什么数据 (queryKey),以及如何获取数据 (queryFn),剩下的复杂性都由 TanStack Query 内部处理。它将服务器状态从应用程序状态中解耦出来,使得应用程序状态管理可以更纯粹地关注客户端 UI 交互逻辑。
高级考量与最佳实践
1. 结构化 Query Key
queryKey 是 TanStack Query 的核心,设计良好的 queryKey 对于缓存管理至关重要。
- 数组结构:
queryKey必须是数组。第一个元素通常是字符串,表示资源类型。 - 包含参数: 后续元素可以是查询参数,确保唯一性。参数的顺序很重要。
- 可序列化:
queryKey中的所有元素都必须是可序列化的。
// 获取所有用户
['users']
// 获取 ID 为 1 的用户
['users', 1]
// 获取 ID 为 1 的用户的待办事项
['users', 1, 'todos']
// 获取状态为 'active' 且按 'createdAt' 排序的所有帖子
['posts', { status: 'active', sortBy: 'createdAt' }]
2. 自定义 Hooks 封装查询逻辑
为了提高代码的可重用性和可维护性,建议将 useQuery 和 useMutation 封装到自定义 Hooks 中。
// hooks/usePosts.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
interface Post { id: number; title: string; body: string; }
// 获取所有帖子的自定义 Hook
export function usePosts() {
return useQuery<Post[], Error>({
queryKey: ['posts'],
queryFn: async () => {
const res = await fetch('https://jsonplaceholder.typicode.com/posts');
if (!res.ok) throw new Error('Failed to fetch posts');
return res.json();
},
staleTime: 1000 * 60 * 10, // 10分钟新鲜
});
}
// 创建帖子的自定义 Hook
export function useCreatePost() {
const queryClient = useQueryClient();
return useMutation<Post, Error, Omit<Post, 'id'>>({
mutationFn: async (newPost) => {
const res = await fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newPost),
});
if (!res.ok) throw new Error('Failed to create post');
return res.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['posts'] }); // 创建成功后使所有帖子列表失效
},
});
}
// 在组件中使用
function MyPostsComponent() {
const { data: posts, isLoading, isError, error } = usePosts();
const createPostMutation = useCreatePost();
// ... 渲染逻辑
}
3. 预加载数据 (queryClient.prefetchQuery)
通过 queryClient.prefetchQuery,我们可以在用户需要数据之前就将其获取并放入缓存,从而优化用户体验。例如,在悬停链接上时预加载目标页面的数据。
import { useQueryClient } from '@tanstack/react-query';
// ... fetchPost 函数定义同上
function PostLink({ postId }: { postId: number }) {
const queryClient = useQueryClient();
const handleMouseEnter = () => {
// 鼠标悬停时预加载帖子详情
queryClient.prefetchQuery({
queryKey: ['post', postId],
queryFn: () => fetchPost(postId),
staleTime: 1000 * 60 * 5, // 预加载的数据也可以设置新鲜时间
});
};
return (
<a
href={`/posts/${postId}`}
onMouseEnter={handleMouseEnter}
>
View Post {postId}
</a>
);
}
4. 服务器端渲染 (SSR) / 静态站点生成 (SSG)
TanStack Query 完全支持 SSR/SSG。你可以在服务器上预取数据,并将预取的数据注入到客户端的 Query Cache 中(hydrate),从而避免在客户端进行首次加载时的闪烁和瀑布流请求。
// 在 Next.js 或 Remix 等框架中
// 获取 QueryClient 实例并预取数据
// export async function getServerSideProps() {
// const queryClient = new QueryClient();
// await queryClient.prefetchQuery({
// queryKey: ['posts'],
// queryFn: fetchPosts, // 你的数据获取函数
// });
// return {
// props: {
// dehydratedState: dehydrate(queryClient), // 将 QueryClient 的状态序列化
// },
// };
// }
// 在客户端组件中
// import { Hydrate, QueryClientProvider, QueryClient } from '@tanstack/react-query';
// function App({ Component, pageProps }) {
// const [queryClient] = React.useState(() => new QueryClient());
// return (
// <QueryClientProvider client={queryClient}>
// <Hydrate state={pageProps.dehydratedState}> {/* 注入服务器端状态 */}
// <Component {...pageProps} />
// </Hydrate>
// </QueryClientProvider>
// );
// }
总结
TanStack Query 通过其强大的 Query Cache 和 Stale-While-Revalidate 策略,为服务器状态管理提供了一个优雅而高效的解决方案。它将数据获取、缓存、同步、错误处理和加载状态等复杂逻辑抽象化,让开发者得以从繁琐的样板代码中解脱出来,专注于业务逻辑的实现。通过声明式的 API 和智能的缓存失效机制,TanStack Query 极大地提升了开发效率和用户体验,成为了现代前端应用中管理服务器状态的首选工具之一。它的出现,标志着前端数据管理领域一次重要的范式转变,将服务器状态与客户端状态清晰地分离,各自采用最适合的工具进行管理。