解析 `TanStack Query`(React Query)的原理:它如何通过缓存失效策略替代传统的全局 Store?

TanStack Query 原理剖析:以缓存失效策略替代传统全局 Store

各位同仁,下午好。今天,我们将深入探讨一个在现代前端开发中日益重要的库——TanStack Query(前身为 React Query)。它在数据管理领域,尤其是在处理服务器状态时,提出了一种与传统全局状态管理库截然不同的范式。我们将聚焦其核心原理:如何通过精妙的缓存失效策略,优雅地替代了我们过去为管理服务器数据而构建的复杂全局 Store 体系。

传统全局 Store 在服务器状态管理中的困境

在深入 TanStack Query 之前,我们有必要回顾一下传统的全局状态管理方案,例如 Redux、MobX 或 Zustand,它们在处理客户端状态方面的强大能力是毋庸置疑的。它们提供了一个单一的、可预测的状态树,使得状态的更新和派发变得清晰。然而,当我们将这些工具应用于管理“服务器状态”时,问题便层出不穷。

服务器状态(Server State)与客户端状态(Client State)有着本质的区别:

  • 异步性: 服务器状态的数据总是异步获取的。
  • 共享性: 多个组件可能依赖同一份服务器数据。
  • 可变性: 服务器数据可能在任何时候被其他用户或系统更改。
  • 缓存需求: 为了性能和用户体验,我们通常需要缓存服务器数据。
  • 数据同步: 客户端缓存的数据需要与服务器保持同步。

传统的全局 Store,在面对服务器状态时,通常需要开发者手动处理以下繁琐而易错的环节:

  1. 加载状态管理: 追踪数据是否正在加载 (isLoading)、是否加载成功 (isSuccess)、是否加载失败 (isError)。
  2. 错误处理: 捕获并展示请求过程中发生的错误。
  3. 数据缓存: 决定何时缓存数据、缓存多久,以及何时清除缓存。
  4. 数据去重: 避免在短时间内重复请求同一份数据。
  5. 数据同步: 当服务器数据发生变化时(例如,通过 POST/PUT/DELETE 请求),如何使客户端缓存的数据保持最新。这通常需要手动触发重新获取或复杂的数据更新逻辑。
  6. 竞争条件: 多个请求同时发出,哪个请求的结果应该被采纳?
  7. 内存管理: 长期不用的数据何时从 Store 中移除以节省内存。

为了解决这些问题,我们往往需要在 Redux 中编写大量的 reducer、action、saga 或 thunk,并手动构建复杂的缓存逻辑。这不仅增加了大量的样板代码,也极大地提升了心智负担。例如,一个简单的用户列表获取,可能需要定义 FETCH_USERS_REQUEST, FETCH_USERS_SUCCESS, FETCH_USERS_FAILURE 三种 action 类型,以及对应的 reducer 逻辑来更新 isLoadingdataerror 状态。当涉及到数据更新后的缓存失效,复杂度更是成倍增长。

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 返回缓存中已有的“过期”数据。

这带来了巨大的用户体验提升:

  1. 即时响应: 用户不会因为等待数据加载而看到空白或加载 Spinners,而是立即看到之前的数据。
  2. 数据最终一致性: 一旦后台重新验证成功,UI 会自动更新为最新数据。

让我们通过一个简单的流程图来理解 SWR:

  1. 组件渲染并请求数据 (useQuery)
  2. 检查 Query Cache:
    • 命中且数据新鲜 (staleTime 未到期):
      • 立即返回缓存数据。
      • UI 渲染。
      • 结束。
    • 未命中或数据过期 (staleTime 已到期):
      • 如果缓存中有旧数据,立即返回旧数据并标记为 isFetching: true
      • 同时 在后台启动 queryFn 重新获取数据。
      • UI 渲染旧数据。
  3. 后台数据获取完成:
    • 成功:
      • 更新 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): 这是控制数据新鲜度的关键参数。
    • 如果 staleTime0(默认值),数据在获取成功后立即被标记为 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)

自动重新获取是基础,但最强大的缓存失效机制是手动触发。这通常发生在数据通过 POSTPUTDELETE 等操作在服务器端发生改变之后。TanStack Query 提供了 queryClient.invalidateQueries 方法来实现这一点。

invalidateQueries 的作用是:

  1. 标记查询为 stale 它会找到所有匹配 queryKey 的查询,并将它们的状态标记为 stale
  2. 触发重新获取: 对于当前正在被组件“观察”(即处于活跃状态)的 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>
  );
}

这个例子完美展示了 setQueryDatainvalidateQueries 的协同作用。用户点击复选框时,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 封装查询逻辑

为了提高代码的可重用性和可维护性,建议将 useQueryuseMutation 封装到自定义 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 极大地提升了开发效率和用户体验,成为了现代前端应用中管理服务器状态的首选工具之一。它的出现,标志着前端数据管理领域一次重要的范式转变,将服务器状态与客户端状态清晰地分离,各自采用最适合的工具进行管理。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注