首页 / 文章 / TanStack Query详细指南:查询、缓存、更新操作及乐观UI实现方式。

TanStack Query详细指南:查询、缓存、更新操作及乐观UI实现方式。

TanStack Query像餐厅经理一样管理服务器状态:在HTTP客户端之上实现共享缓存键、新鲜度控制、协同更新、失效处理以及乐观更新功能。

5069 词

今天的主题是TanStack Query(原名React Query):这款库能够将混乱的服务器状态获取流程转化为可预测的缓存、数据新鲜度及数据修改工作流。用高端餐厅作类比就能让人更容易记住其中的各个环节——餐厅大厅对应UI,厨房对应后端,服务员对应HTTP客户端,而经理则相当于TanStack Query。

什么是TanStack Query?

在典型的应用中,UI会通过fetch或Axios向后端请求数据。这些客户端在协调方面并不智能。如果五个组件同时请求相同的菜单,就需要进行五次独立的厨房取餐操作。如果有人点了当天的汤,十秒后又有另一位客人点同样的汤,那些不够聪明的服务员就会再次回到厨房取餐。这不仅会让服务器负担过重,还会降低餐厅的服务效率。

TanStack Query就像餐厅的主厨与经理:它负责协调数据的获取、缓存、同步和更新服务器状态,从而避免厨房被重复的请求困扰。

Axios / Fetch 与 TanStack Query 的区别

初学者常常认为 TanStack Query 可以替代 Axios 或 fetch,其实并非如此。

  • Fetch 和 Axios 相当于服务员。他们将请求送到“厨房”并带回响应,但不会记住之前的请求、判断数据的新鲜度,也不会协调其他服务员。
  • TanStack Query 则是经理。它安排服务员执行任务,记住返回的数据、判断其是否仍保持新鲜、协调哪些桌子使用同一份记录本,以及在厨房修改菜品后何时派人再去取新数据。

你仍然需要编写调用 Axios 或 fetch 的 queryFn 代码。TanStack Query 会通过缓存键、去重机制、重试功能以及生命周期钩子来封装这些调用。

什么是 TanStack Query?(功能概览)

在日常的 React 开发中,有六项功能最为重要:

1. 查询(useQuery):获取数据

通过 queryKey 进行声明式读取,具体操作由 queryFn 执行。

2. 缓存:管理器的核心

查询结果会以查询键为标识存储在内存中,这样多个组件就可以共享一次网络请求。

3. 数据新鲜度:staleTime 与 gcTime

staleTime 决定了何时认为缓存数据已过时需要重新获取。而 gcTime(垃圾回收)则决定了未被使用的缓存条目在被清除之前能保留多久。

4. 变更操作(useMutation):修改数据

写入操作——创建、更新、删除——会在需要时执行,而非在组件加载时。

5. 查询失效处理:更新菜单内容

在变更操作成功后,将相关的查询标记为过期,从而使用户界面与服务器重新同步。

6. 乐观更新:类似米其林星级服务的体验

立即更新缓存,出错时回滚,并最终完成彻底同步。

本指南的其余部分将通过餐厅场景及具体代码示例来讲解各项功能。

1. 查询操作(useQuery):获取菜单内容

查询用于指定所需数据及其获取方式:

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

// The waiter function (Axios)
const fetchMenu = async () => {
  const response = await axios.get('/api/menu');
  return response.data;
};

function MenuComponent() {
  // The Manager (TanStack Query) orchestrating the process
  const { data: menu, isLoading, isError, error } = useQuery({
    queryKey: ['menu'], // The label for this specific data
    queryFn: fetchMenu,  // The waiter doing the fetching
  });

  if (isLoading) return <div>Waiter is walking to the kitchen... Loading Menu...</div>;
  if (isError) return <div>The kitchen is on fire! Error: {error.message}</div>;

  return (
    <ul>
      {menu.map((item) => (
        <li key={item.id}>{item.name} - ${item.price}</li>
      ))}
    </ul>
  );
}

queryKey 是管理员记事本中的文件名——如 ['menu']、['soup']、['allergies', tableId]。相同的键会共享缓存,并对正在处理的请求进行去重处理。queryFn 则相当于服务员的行动流程:返回一个数据承诺。

在首次获取数据时,isPending 和 loading 标志允许界面显示占位符。错误会通过 isError 和 error 展现出来。成功获取的数据会出现在 data 中,可供所有监听该键的组件使用。

什么是竞态条件?

餐厅类比

想象有两位客人同时点汤,而厨房处理速度很慢。A桌的延迟响应绝不能覆盖B桌更早提交的请求。如果没有协调机制,无论哪个承诺最后完成都会胜出——即便它的数据已经过时。

在 React 中的实现方式(useEffect)

手动使用 useEffect 进行数据获取时常常会忽略取消逻辑。当在页面间快速切换时,较旧的响应可能在新的请求开始后就已经修改了状态。

TanStack Query 如何解决竞态问题

该库通过键值来追踪正在处理的查询,可在支持的情况下使用 AbortSignal 进行取消,并确保 UI 监听器看到的是连贯的缓存状态变化,而非零散的 setState 竞态现象。

2. 缓存:管理器的备忘录

// Waiter function
const fetchMenu = async () => {
  console.log("Waiter is walking to the kitchen!"); // We can track how many times this runs
  const response = await axios.get('/api/menu');
  return response.data;
};

// Component 1: The Sidebar
function MenuSidebar() {
  const { data } = useQuery({ queryKey: ['menu'], queryFn: fetchMenu });
  return <div>We have {data?.length} items today!</div>;
}

// Component 2: The Main Display
function MenuMainDisplay() {
  const { data } = useQuery({ queryKey: ['menu'], queryFn: fetchMenu });
  return <div>{data?.map(item => <p>{item.name}</p>)}</div>;
}

当第一个组件使用 ['menu'] 加载时,管理器会发送一个等待任务。几毫秒后如果有另一个组件使用相同的键加载,它会直接读取备忘录而无需再次查询。正是这种去重机制,使得包含众多共享用户或配置查询的仪表板无需自定义全局存储就能保持快速响应。

管理器的实际运作流程(分步说明)

  1. 组件A加载 → 缓存未命中 → 请求网络。
  2. 响应到达 → 写入缓存 → 组件A开始渲染。
  3. 使用相同键的组件B加载 → 缓存命中 → 组件B立即渲染。
  4. 根据新鲜度规则,稍后可能会进行后台数据重新获取,但不会阻塞组件B的首次渲染。

服务器状态应存储在TanStack Query中;而真正的客户端UI状态(如模态框是否打开、当前选中的标签页)则可以保存在React状态或轻量级客户端存储中。

3. 新鲜度:配置staleTime和gcTime

1. staleTime:这些信息仍然准确吗?

const { data } = useQuery({
  queryKey: ['soup'],
  queryFn: fetchSoup,
  staleTime: 1000 * 60 * 30, // 30 minutes
});

通过设置staleTime: 10_000,十秒以内的数据被视为新鲜数据:重新加载时无需再次获取即可直接使用。一旦这些数据过期,观察者可以在特定条件下触发后台重新获取操作(如重新加载、窗口获得焦点或重新连接——具体取决于默认设置和选项)。staleTime的值应根据数据的变化频率来确定:每日特供内容的可能更新频率较高,而国家列表则可能更新较慢。

2. gcTime:我可以丢弃这些数据吗?

const { data } = useQuery({
  queryKey: ['allergies', 'table4'],
  queryFn: fetchAllergies,
  gcTime: 1000 * 60 * 60 * 24, // Keep in memory for 24 hours
});

gcTime决定了在所有观察者都停止监听后,缓存条目仍会在内存中保留的时间。较短的gcTime能更快释放内存;较长的时间则能让再次访问时立即获取数据。请勿将其与staleTime混淆:过期数据仍会保留在内存中,直到被垃圾回收。

终极秘诀:stale-while-revalidate

TanStack Query会在后台重新获取数据时显示过时的内容。用户能立即看到最新的菜单信息;一旦后厨确认有更新,记事本就会刷新。正是这种机制使得该库每次使用时都比加载指示器更快。

4. 变更操作(useMutation):添加新菜品

什么是变更操作?

变更操作会改变服务器状态——比如下订单、编辑个人资料、删除评论等。

以餐厅为例:下订单

服务员不会在客人坐下时自动下单,而是会等待明确的请求。变更操作也是如此:只有当你调用mutate或mutateAsync时才会执行。

代码示例:构建订单表单

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

// 1. The Waiter Function (The actual network request)
const placeOrder = async (orderData) => {
  // We are using POST because we are creating a new order
  const response = await axios.post('/api/orders', orderData);
  return response.data;
};

function OrderForm() {
  const [dish, setDish] = useState('');

  // 2. The Manager orchestrating the mutation
  const mutation = useMutation({
    mutationFn: placeOrder,
    // We can also trigger side effects right here!
    onSuccess: (data) => {
      console.log("Chef says: Order confirmed!", data);
    },
    onError: (error) => {
      console.log("Chef says: We have a problem.", error.message);
    }
  });

  const handleSubmit = (e) => {
    e.preventDefault();
    // 3. Triggering the mutation and passing the variables
    mutation.mutate({ dishName: dish, tableNumber: 4 });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={dish}
        onChange={(e) => setDish(e.target.value)}
        placeholder="What would you like?"
      />

      {/* Notice how we use isPending to disable the button so they don't double-order! */}
      <button type="submit" disabled={mutation.isPending}>
        {mutation.isPending ? 'Sending to Kitchen...' : 'Place Order'}
      </button>

      {/* Handling the feedback */}
      {mutation.isError && <p style={{ color: 'red' }}>Failed: {mutation.error.message}</p>}
      {mutation.isSuccess && <p style={{ color: 'green' }}>Order placed successfully!</p>}
    </form>
  );
}

将onSuccess与提示反馈、导航或无效化操作关联起来。在提交处理程序中需要等待操作完成时,请使用mutateAsync。

高级开发者详情

1. 它不会自动运行

与查询不同,变更操作在未被调用时会处于待命状态——从而避免在渲染过程中发生意外写入。

2. isPending与isLoading

在v5版本中,建议使用isPending来表示变更操作正在进行中的状态,并根据该标志来控制界面的禁用状态。

3. 防止双击错误

当isPending为真时禁用提交按钮,这样用户就无法重复下单。

5. 查询无效化:告知管理器更新记事本

问题:过时的记事本

厨师添加新菜品后,仍在查看缓存菜单的桌位会继续显示昨天的菜单列表,直到有内容被重新获取。

解决方案:查询失效

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

function AddDishForm() {
  // 1. Get access to the Manager's office
  const queryClient = useQueryClient();

  const mutation = useMutation({
    mutationFn: async (newDish) => {
      const response = await axios.post('/api/menu', newDish);
      return response.data;
    },
    // 2. The magic happens HERE in the onSuccess callback
    onSuccess: () => {
      // 3. Tell the Manager to rip up the menu notepad
      queryClient.invalidateQueries({ queryKey: ['menu'] });
      console.log("Menu invalidated! The Manager is getting a fresh copy.");
    },
  });

  // ... form code
}

invalidateQueries会将匹配的条目标记为过期,并触发活跃观察者的重新获取操作。

调用invalidateQueries时究竟会发生什么?

匹配的查询会变为过期状态;已挂载的观察者会重新获取数据;未挂载的条目则等待下次挂载(受gcTime限制)。厨房仍是数据来源,而记事本则会被要求刷新。

更高级的概念:模糊匹配

精确失效:

queryClient.invalidateQueries({ queryKey: ['menu', 'lunch'] });

或者使整个前缀失效:

// This rips up the breakfast, lunch, and dinner notepads all at once!
queryClient.invalidateQueries({ queryKey: ['menu'] });

当您广泛地使['menu']失效时,模糊前缀匹配机制会破坏早餐、午餐和晚餐相关的笔记内容——这种功能虽强大却十分危险。建议使用最精确的键值,以确保用户界面正常显示。

变更操作的金科玉律

每一次成功的写入操作都应要么使依赖它的读取操作失效,要么精确地更新缓存。若不处理读取操作,用户界面在数据保存后就会出现错误显示。

6. 乐观更新:米其林星级体验

类比说明:经理的信任

一位值得信赖的经理可以在厨房确认之前就在账单上写下新啤酒的种类,但如果酒柜里没有该啤酒,则会将其擦掉。

乐观更新的三大支柱

在useMutation函数内部:

  1. onMutate:阻止冲突的请求,对缓存进行快照保存,立即写入乐观版本的数据。
  • onError:如果服务器拒绝请求,则恢复快照。
  • onSettled:无论操作成功与否,都使缓存失效(或以其他方式同步),确保缓存与数据库保持一致。
  • 代码示例:乐观地添加菜品

    import { useMutation, useQueryClient } from '@tanstack/react-query';
    import axios from 'axios';
    
    function AddDishForm() {
      const queryClient = useQueryClient();
    
      const mutation = useMutation({
        mutationFn: async (newDish) => {
          const response = await axios.post('/api/menu', newDish);
          return response.data;
        },
    
        // 1. The millisecond the user clicks submit...
        onMutate: async (newDish) => {
          // A. Cancel any outgoing refetches so they don't overwrite our optimistic update
          await queryClient.cancelQueries({ queryKey: ['menu'] });
    
          // B. Take a snapshot of the current menu (The Eraser Backup)
          const previousMenu = queryClient.getQueryData(['menu']);
    
          // C. Optimistically update the Manager's notepad right now!
          queryClient.setQueryData(['menu'], (oldMenu = []) => {
            // We fake an ID for now, the real ID comes from the database later
            return [...oldMenu, { ...newDish, id: Math.random().toString() }];
          });
    
          // D. Return the snapshot so onError can use it if things go wrong
          return { previousMenu };
        },
    
        // 2. If the Kitchen catches on fire...
        onError: (err, newDish, context) => {
          // Use the eraser! Roll back to the snapshot we saved in onMutate
          if (context?.previousMenu) {
            queryClient.setQueryData(['menu'], context.previousMenu);
          }
          console.error("Chef says no! Rolling back.", err);
        },
    
        // 3. Always run this at the very end, success or fail...
        onSettled: () => {
          // Tell the Manager to get the real, final menu from the database
          queryClient.invalidateQueries({ queryKey: ['menu'] });
        },
      });
    
      // ... form code
    }
    

    需要注意正在处理的查询的取消、快照结构以及回滚路径。虽然乐观式用户界面给人的感觉是即时响应的,但当后端拒绝请求时绝不能让缓存陷入困境。

    整合核心要素

    • TanStack Query是一个异步状态管理器,而非数据获取工具。它封装了Axios/fetch,从而让网络操作更加可预测。
    • 查询键至关重要。它决定了数据的去重、缓存机制以及组件间的共享方式。
  • 写入后的缓存同步是你的职责。使用invalidateQueries或精心设计的乐观更新机制,确保客户端数据与服务器端保持一致。
  • 实际应用中的常用默认设置

    对于主要为静态的资源,设定合理的staleTime值(以分钟为单位);而对于用户特定的易变数据,则设置较短或为零的staleTime值。保持queryFn的纯函数特性且能够被中止执行。将键值集中管理在工厂函数中(如menuKeys.list()、menuKeys.detail(id)),从而保证缓存失效操作的类型安全与一致性。在开发阶段,若发现重复请求,应记录缓存相关事件以便诊断问题。除非屏幕确实需要复杂的优化功能,否则优先使用缓存失效机制,而非手动编写复杂的缓存处理代码。

    常见故障模式

    • 使用不稳定的键值(每次渲染都生成新的对象字面量)会导致缓存失效。
    • 在数据发生变更后忘记进行失效处理会导致出现虚假数据。
    • 若不设置相应的变更策略却将staleTime设为无限值,会使得用户界面无法正常更新。
    • 将所有客户端界面相关标志都放入查询缓存中,会混淆服务器状态与客户端状态的边界。
    • 采用过宽范围的失效处理方式(如queryKey: ['']这样的错误写法)会导致整个数据集被重新加载。

    避免这些问题,餐厅就能正常运转:服务员能在需要时及时行动,经理的记事本内容保持一致,顾客也能随时看到热食,而无需每隔十秒就盯着厨房门看。

    总结

    TanStack Query 能够立足市场,是因为它掌控了服务器端的生命周期管理——包括读取、数据新鲜度、写入以及同步操作——而传输层则交由 Axios 或 fetch 处理。你需要了解相关控制键、数据新鲜度调节参数(staleTime、gcTime)以及写入操作流程(变更、失效处理、乐观更新)。有了这些功能,React 应用便无需在每个 useEffect 中重复实现请求缓存逻辑,从而能像管理有序的餐厅那样正常运行。

    为何“餐厅”比喻始终适用

    在想象五个服务员同时冲向同一份汤之前,网络级联的概念似乎很抽象。去重操作就好比经理抬手示意:只需一次走动,就能为多张桌子服务。过时重置机制则是当跑堂员正在查看菜单板时,继续提供最后打印的菜单。失效处理则是指厨师更改食谱时撕掉相关页面。乐观更新则是先在账单上写下顾客的订单,等待确认后再用橡皮擦掉。在指导新人时,先让他们想象这些场景,然后再讲解 TypeScript 泛型,这样理解会更牢固。

    与路由器和认证系统集成

    当数据并非全局共享时,键值应包含租户或用户身份:['menu', restaurantId] 或 ['allergies', userId]。登出时请清除缓存,以避免不同用户之间的笔记页面信息泄露。使用 React Router 或类似框架时,应在已知哪些资源发生变更的操作中触发无效化处理,而非每次导航都重新获取所有数据。

    测试策略

    应单独对 queryFn 映射函数进行单元测试。在组件测试中,使用全新的 QueryClient 并设置 retry: false 以保障测试结果的可重复性,同时用 QueryClientProvider 将其包裹起来。需验证变更操作会使用预定的键值调用 invalidateQueries 函数。对于乐观更新路径,应模拟服务器错误并确认系统能回滚到之前的快照状态。切勿在未经重置的情况下,在相互无关的测试之间共享同一个 QueryClient。

    性能注意事项

    大量数据应通过分页或无限查询来处理,而非依赖一个巨大的键值。选择器(select)能让组件在无需重新渲染无关缓存字段的情况下查看数据片段。确保queryFn的返回结果具有序列化性且稳定。在繁忙的控制台环境中,当refetchOnWindowFocus与极短的staleTime结合使用时,需监测数据重新加载的频繁情况——应针对每个查询进行调整而非全局设置。

    从原始useEffect向新架构的迁移思路

    用useQuery替换那些用于设置加载状态/错误状态/数据状态的挂载效果。用useMutation替换命令式的POST处理逻辑。删除自制的缓存机制。保留用于拦截器和身份验证头的Axios实例,并将它们传递给queryFn。迁移应逐步进行:一次只修改一个页面,这样能更快减少竞态条件导致的错误。

    功能上线前的最终检查清单

    1. 稳定且分层的 queryKey。
    2. 为该域明确指定 staleTime 值。
    3. 将变更操作与失效处理或乐观更新机制结合使用。
    4. 待处理状态会阻止重复提交。
    5. 错误提示已妥善设置。
    6. 会话结束时,与认证相关的密钥会被清除。

    只要满足这六点,TanStack Query 就不再只是“另一个库”,而会成为你的应用所急需的完美管理工具。

    详解:在三个组件之间加载菜单

    想象这样一个界面:顶部显示当天的汤品,侧边栏列出午餐特惠,主区域则展示完整菜单。如果没有 TanStack Query,每个区域都可能需要自行创建 useEffect 并向 /api/menu 发送请求。而通过共享的 queryKey: ['menu', restaurantId],首次加载时会进行网络请求以获取数据,后续加载则直接从缓存中读取。当厨师通过管理表单使用 useMutation 修改汤品内容时,['menu', restaurantId] 的失效会触发屏幕上所有区域的刷新。这样,顾客就不会因为不同服务员的意见分歧而看到三种不同的汤品。

    这个案例涵盖了数据去重、共享缓存、数据变更以及失效处理等功能。大多数实际应用界面都是它的变体:个人资料头部加上设置表单、购物车图标加上结账项列表、通知铃铛加上通知页面等。

    将查询键设计为类似文件路径的结构

    应将键视为分层路径:

    • ['menu', restaurantId]
    • ['menu', restaurantId, 'lunch']
    • ['menu', restaurantId, 'item', itemId]
    • ['allergies', restaurantId, tableId]

    工厂模式能提供帮助:

    若配置为允许,使['menu', restaurantId]失效时仍可进行模糊匹配以定位更深层的键,这样就能同时保存列表视图和详细信息视图。避免在键中嵌入不可序列化的值(如函数、类实例),应优先使用原始ID和稳定的枚举类型。

    根据产品语言选择staleTime

    询问产品负责人,UI在N秒内出现错误的程度有多大。那些每月都会变动的营销文案可以承受较长的数据过期时间。而限时抢购时的库存统计则需要将staleTime设置得尽可能接近零,并在每次数据变更时立即失效。请在相关查询旁记录这一选择,以免后续编辑者将易变的查询“优化”为具有较长过期时间的结构。

    gcTime是控制内存使用的重要参数。那些包含众多页面路径的移动应用,若能短暂保留最新页面,就能让返回上一页的操作显得即时响应。而在内存有限的设备上,庞大的缓存则需要更短的gcTime或分页机制。

    看似安全的数据变更

    始终要显示待处理状态和错误状态。在处于待处理状态时,应禁用可能造成破坏的按钮。对于删除操作,如果服务器返回409或500状态码,则应通过乐观删除机制恢复快照;对于创建操作,若成功则需将临时客户端ID替换为服务器生成的ID——如果ID映射过于复杂,则可直接跳过乐观机制并使数据失效。

    对同一键值进行的并行修改可能会相互冲突,应将这些操作排入队列或禁用相关控件。表单库中的mutateAsync方法应放在提交处理函数的try/catch块中,而非渲染阶段。

    可扩展的失效机制

    登录后,如果仍需显示公共内容,则应使用户级密钥失效而非清除整个客户端。登出时,通常使用 queryClient.clear() 即可。当 WebSocket 发送“菜单已更改”信号时,应调用与 HTTP 变更操作相同的失效处理函数,以便两种方式采用统一的同步策略。

    对于可能需要的详情页,可在悬停时进行预取:queryClient.prefetchQuery({ queryKey, queryFn }) 能将感知到的延迟转化为缓存命中,且无需修改页面代码。

    无需神话色彩的乐观更新

    并非每个 POST 操作都必须采用乐观更新机制。当正常流程较为常见、界面改进效果明显且易于实现回滚时,才可使用该机制。若服务器验证较为复杂,或响应体是渲染所必需的(如服务器生成的编号、价格、税费),则应避免使用。相比错误数据的瞬间显示,缓慢的加载提示反而更合适。

    在使用乐观更新时,要保持快照不可变,在onMutate中取消冲突的查询,并始终在onSettled中进行同步。在开发阶段要记录回滚操作;无声的回滚会让质量保障人员感到困惑。

    与全局客户端存储的对比

    Redux或Zustand可以存储服务器数据,但你需要重新创建缓存、处理请求去重以及进行后台刷新。TanStack Query则专注于这一领域。将临时的UI数据保存在本地状态或小型客户端存储中,而将服务器实体保存在查询缓存中。混用这两种数据源会导致信息不一致。

    培训团队

    举办实战训练:构建一个包含列表查询、详情查询、创建操作、数据失效处理以及乐观创建功能的简单菜单应用。要求使用键生成函数并实现登出清空功能。一旦这种模式成为团队的肌肉记忆,大型应用就不会再出现由useEffect带来的获取数据错误。

    以文字形式呈现的概要表

    查询用于读取数据,修改操作用于写入数据。键值用于标识缓存中的行。staleTime用来判断“是否可以无需询问就重复使用?”gcTime则用于判断“是否可以丢弃这张便签页?”无效性检测用于提示“后端已变更——需要刷新。”乐观策略则认为“现在就更新账单,若被拒绝则删除。”数据传输仍使用 Axios 或 fetch。这种分工方式构成了整个产品。

    端到端场景:午餐特惠板

    一家餐厅开始午餐时段营业。特色菜展示板使用 queryKey: ['menu', restaurantId, 'lunch'],并设置两分钟的staleTime,因为在一个营业时段内黑板上的内容变化较慢。汤品标题组件则使用 ['menu', restaurantId, 'soup'],其失效时间为三十秒。这两个queryFn都会调用带有身份验证拦截器的同一个Axios实例。当管理员通过useMutation保存新的汤品时,onSuccess会使这两个键失效——如果目的是进行模糊匹配,则会令共享的前缀['menu', restaurantId]失效。所有正在使用的平板设备上的顾客都能无需手动刷新即可看到更新内容。

    如果管理员表单使用的是自行实现的fetch函数且不进行失效处理,那么平板设备上的数据将会保持错误状态直到重新加载。而TanStack Query存在的意义正是为避免这种错误情况。

    TypeScript中的查询键生成器

    键值集中管理:

    • menuKeys.all(restaurantId)
    • menuKeys.lunch(restaurantId)
    • menuKeys.item(restaurantId, itemId)

    工厂模式可避免拼写错误,并便于在代码库中查找无效的键值。建议使用基本类型的元组。当存在过滤器时,应包含按稳定顺序序列化的过滤器对象。除非选项对象已被缓存且可序列化,否则切勿将其全部放入键值中。

    你实际会使用的useQuery选项

    queryKey与queryFn之外:enabled参数可控制仅在存在对应ID时才进行数据获取;retry用于设定临时性故障的处理策略;对于计算成本较高的仪表板,可禁用refetchOnWindowFocus功能;placeholderData或initialData有助于保持界面布局稳定;select功能可筛选所需数据,从而减少页面重绘次数。初始时使用默认设置即可,若发现某些查询存在频繁重新获取数据的情况,则需进行相应调整。

    从用户体验角度理解“在验证期间仍显示旧数据”机制

    在重新获取数据的过程中仅显示100毫秒前的购物车总价可能是不可接受的,而显示1分钟前的帮助中心文章则并无问题。这类设置应通过staleTime参数来实现,而非依靠临时的标志位。除非用户明确要求,否则后台数据重新获取失败不应清除那些有效的旧数据;在离线状态下,用户更愿意看到稍显陈旧的页面内容,而非不断闪烁的加载错误提示。

    变异:实体提交的结构分析

    在处理中状态时禁用按钮;从error中显示内联错误信息;成功后使缓存失效或更新缓存;在状态确定后,如适用则清除本地表单状态。在表单库的提交处理程序中使用mutateAsync并配合try/catch结构。在没有并发控制的情况下,切勿在循环中调用mutate。对于上传操作,需单独显示进度——TanStack Query跟踪的是变异状态而非字节传输进度。

    失效粒度的相关说明

    粒度过窄:仅更新列表而忽略详细信息→详细页面内容失效。粒度过宽:['menu']下的每个键都会被重新获取→导致性能严重下降。应根据能够显示不一致性的界面来调整粒度。如有疑问,直接使列表及所修改的详细信息ID失效。添加前缀进行失效处理是为了有意识地实现数据扩散。

    乐观更新的陷阱

    快照必须深度克隆足够的结构,以便恢复嵌套列表。临时的客户端标识绝不能泄露到服务器上。如果多个乐观更新操作同时进行,回滚操作可能会相互干扰——需为这些流程序列化用户界面。即便操作成功,也必须在最终确认时与服务器上的数据保持一致,因为服务器可能会对你未发送的字段进行规范化处理。

    React 严格模式与双重挂载

    在开发模式下,严格模式会两次触发效果函数。TanStack Query 会根据键值来去重,因此不应出现针对同一键值的重复网络请求。如果出现了这种情况,说明你的键值不稳定,或是 queryFn 的标识问题导致了去重机制失效。调试时请记录键值信息。

    SSR与数据注入注意事项

    对于 Next.js 及类似框架,应在服务器端对查询客户端进行脱水处理,在客户端再重新水合,这样才能确保在页面跳转后笔记应用仍能正常运行。需确保 queryFn 在两种环境中都能执行,或提供服务器预取功能在渲染前填充缓存。服务器端与客户端之间的数据结构不一致会导致水合警告,这些警告看似是框架本身的缺陷,实则为缓存初始化问题。

    与手动实现的 SWR 模式的对比

    许多团队会重新设计该库的某些功能:缓存映射、聚焦时重新获取数据、修改数据后重新验证。TanStack Query 通过社区默认配置和开发者工具将这些实现方式标准化了。只有对于小型应用或特殊运行环境,才有必要手动实现相关功能;否则,使用现成方案能节省大量时间。

    开发者工具与可观测性

    React Query 开发工具会显示键值、数据过期状态、观察者以及获取请求的进度。应在添加控制台日志之前先教会团队如何解读这些信息。在生产环境中,需从错误报告中删除敏感数据;当隐私要求允许时,应记录查询失败时的键名而非完整数据内容。

    反模式检查清单

    键值不稳定;缺少无效化机制;没有同步变更导致数据永久过期;将 UI 状态标志存储在服务器缓存中;无效化范围过宽;使用乐观更新却未设置回滚机制;在尚未生成 ID 时忽略 enabled 参数;用变更操作来执行读取操作。避免这些错误,就能保持系统的有序运行。

    给快速浏览者的总结

    服务员负责传输数据。管理者则负责记忆与协调操作。密钥记录在记事本页面中。新鲜度调节器控制数据的重复使用。变异操作用于写入数据。无效化机制与乐观主义原则确保记事本的准确性。这就是 TanStack Query 的核心概要——也是它选择封装 Axios 而非直接替代它的原因。

    额外的厨房练习题

    重新构建一个小型应用:实现列表查询、详情查询,编写带有无效化机制的创建变异操作,再实现带有强制错误路径的乐观创建操作。添加可清除客户端数据的登出功能。在列表项悬停时添加预加载功能。使用开发者工具测量共享密钥前后网络调用的情况。这些练习比单纯阅读 API 文档更能帮助深入理解该库。

    在审查 Pull Request 时,要问:密钥是什么?staleTime 是什么以及为何存在?写入数据后哪些内容会被标记为无效?待处理状态是否会阻止重复提交?如果这些问题的答案清晰明确,那么该功能在并发操作和导航压力下仍能正常运行。

    带来即时感的预取模式

    可在路由悬停时、用户点击前焦点切换时,或登录后针对默认控制面板查询进行预取。这种预取方式无需设置观察器即可填充缓存。当用户切换页面时,useQuery会直接找到已缓存的最新数据,从而跳过加载占位符。若预取的键错误,则会浪费带宽;而预取正确的键则能在不修改queryFn代码的情况下提升用户感知的性能。

    应将预取与合理的staleTime值结合使用。若预取的数据很快就会过期,虽然仍能实现即时后台重取,但依然比从零开始加载要好,却并非完全免费。应优先预取关键路径的查询数据,而无需为那些少用的设置界面进行预取。

    依赖查询与流水线控制

    当细节数据需要从列表选择中获取标识符时,可使用 enabled: !!selectedId 来控制。若第二个查询需要第一个查询的数据,则需谨慎处理:要么将第二个键与第一个查询的结果标识符嵌套在一起,要么在 API 支持的情况下使用单个 queryFn 来同时返回两种格式的数据。级联查询会增加总响应时间;在条件允许的情况下,使用带有共享认证头的并行查询更为合适。

    Suspense 模式会改变加载边界的结构。如果团队使用 Suspense,需确保错误处理机制与它保持一致,并且查询出错时能按预期抛出异常。同时使用 Suspense 和传统加载指示器会让审核人员感到困惑——请在每个路由树中选择一种风格。

    分页、无限滚动查询与缓存页面

    列表页面通常在键中使用页面参数:['orders', { page, pageSize, status }]。切换页面会创建新的缓存条目;使用placeholderData: keepPreviousData(或当前 API 中的等效选项)可保留之前的数据,避免表格突然显示为空。无限滚动查询会不断添加页面;需谨慎处理失效操作,以免意外清除滚动位置。当某次修改仅涉及一行数据时,可根据排序规则的敏感度选择修补该页面条目或使整个列表失效。

    错误恢复与重试用户体验

    默认的重试机制有助于应对不稳定的移动网络。对于401/403错误,应禁用重试并引导用户登录。遇到详情页的404错误时则需立即报错。为高级用户,可在支持界面中显示failureCount和failureReason信息。全局的QueryCache监听器每次只针对一个键生成提示信息,而非每个观察者都生成——这样就能避免当五个组件共享同一个出错的查询时出现大量提示信息的情况。

    测试策略

    在单元测试中,使用带有retry: false参数的新的QueryClient,并设置较短的垃圾回收间隔。可通过模拟queryFn或使用MSW来实现测试。需验证加载、成功和错误状态。对于数据变更操作,要确认已使用预期的键调用了invalidateQueries方法。集成测试则应确保共享同一键的两个组件不会重复请求数据。不稳定的测试结果往往源于测试用例之间的缓存残留——因此每次测试都应创建新的客户端。

    版本升级与API变化

    TanStack Query从v4升级到v5时,部分选项的名称发生了变化,默认值也有所调整。在升级过程中,请阅读迁移指南,更新Devtools包,并重新检查keepPreviousData / placeholderData的使用情况。应在锁定文件中指定版本号。将查询键格式的变更视为破坏性变化:部署后旧的缓存数据可能不再与新键匹配——可以接受一次性出现冷缓存的情况,或者为键的前缀添加版本标识。

    生产环境问题中的注意事项

    有一个团队遇到重复提交的问题,原因是当另一个变更实例的isPending状态为真时,提交按钮仍处于可用状态。另一个团队在登出时错误地创建了新客户端却未清除旧的提供者引用,从而导致整个缓存被清空。还有一个团队将用户对象编码到键中,破坏了结构共享机制。应将这些问题记录在入门文档中,这样新成员就能避免重复犯错。

    请记录系统的默认设置:默认的staleTime值、哪些查询是用户级范围的、登出时如何清除状态,以及何时允许乐观更新。一致性比巧思更重要。

    生产事故中的现场记录

    有一个团队遇到重复提交的问题,原因是当另一个变更实例的isPending状态为真时,提交按钮仍处于可用状态。另一个团队在登出时错误地创建了新客户端却未清除旧的提供者引用,从而导致整个缓存被清空。还有一个团队将用户对象编码到键中,破坏了结构共享机制。应将这些问题记录在入门文档中,这样新成员就能避免重复犯错。

    请记录系统的默认设置:默认的staleTime值、哪些查询是用户级范围的、登出时如何清除状态,以及何时允许乐观更新。一致性比巧思更重要。

    生产事故中的现场记录

    有一个团队遇到重复提交的问题,原因是当另一个变更实例的isPending状态为真时,提交按钮仍然处于可用状态。另一个团队在登出时错误地创建了新客户端却未清除旧的提供者引用,从而导致整个缓存被清空。还有一个团队将用户对象编码到键中,破坏了结构共享机制。应将这些问题记录在入门文档中,这样新成员就不必重复经历同样的错误。

    请记录系统的默认设置:默认的staleTime值、哪些查询是用户范围的、登出时如何清除状态,以及何时允许乐观更新。一致性比巧思更重要。