Skip to content
Go back

React Query 如何维护复杂项目页面中的数据一致性

项目管理工具与普通内容页面的区别,在于同一份业务数据会同时出现在许多位置:项目列表、项目详情、任务看板、成员筛选器和统计卡片都可能依赖同一个项目或任务状态。

如果每个页面自行请求并永久保存一份数据,用户更新项目以后,很容易看到列表已经变化而详情仍停留在旧状态。React Query 适合管理这类服务端状态,但它并不会自动知道哪些数据彼此相关,查询键与同步策略仍需要围绕业务设计。

先区分服务端状态和本地状态

项目数据、任务列表和成员权限来自服务端,具有缓存、过期和重新拉取的需求。抽屉是否打开、当前选中的 Tab、表单尚未提交的输入则属于本地界面状态。

交给 React Query:  项目列表、项目详情、任务状态、成员数据、统计结果留给组件或轻量状态容器:  弹层显示、视图模式、临时筛选展开、编辑中的草稿

如果把所有数据都当成全局客户端状态维护,页面会不断手写请求、缓存和同步逻辑;如果把临时交互状态也当作服务端查询处理,组件又会变得绕远。

查询键是缓存关系的基础

项目列表通常受筛选、分页和排序条件影响,详情则由项目标识决定:

const queryKeys = {  projectList: params => ["projects", "list", params],  projectDetail: projectId => ["projects", "detail", projectId],  tasks: projectId => ["projects", projectId, "tasks"]};
function ProjectList({ filters }) {  const query = useQuery({    queryKey: queryKeys.projectList(filters),    queryFn: () => projectService.fetchList(filters)  });  return <ProjectTable data={query.data?.items || []} />;}

查询键需要包含真正决定返回结果的条件。缺少筛选条件,会让不同列表错误共享缓存;把无关的临时对象放入键中,又会造成无意义的重复查询。

更新操作完成后要同步相关视图

例如在详情页修改项目名称,受到影响的不只有详情缓存,列表中同一个项目的名称也应该更新。最简单可靠的做法是在保存成功后让相关查询失效:

function useUpdateProject() {  const queryClient = useQueryClient();  return useMutation({    mutationFn: projectService.updateProject,    onSuccess: (_, variables) => {      queryClient.invalidateQueries({        queryKey: queryKeys.projectDetail(variables.id)      });      queryClient.invalidateQueries({        queryKey: ["projects", "list"]      });    }  });}

失效后重新请求能够保证结果以服务端为准,适合更新并不十分频繁、但正确性重要的管理场景。

即时反馈可以使用乐观更新

某些操作非常明确,例如任务完成状态切换。为了让用户立即获得反馈,可以先更新缓存,失败后再回滚:

useMutation({  mutationFn: taskService.toggleCompleted,  onMutate: async variables => {    const key = queryKeys.tasks(variables.projectId);    await queryClient.cancelQueries({ queryKey: key });    const previous = queryClient.getQueryData(key);    queryClient.setQueryData(key, old =>      updateTaskCompleted(old, variables.taskId, variables.completed)    );    return { key, previous };  },  onError: (_, __, context) => {    queryClient.setQueryData(context.key, context.previous);  },  onSettled: (_, __, variables) => {    queryClient.invalidateQueries({      queryKey: queryKeys.tasks(variables.projectId)    });  }});

乐观更新不适合所有操作。涉及复杂权限判断、服务端计算或多对象联动的修改,先等待保存成功再刷新会更稳妥。

两个时间参数控制的是不同问题

React Query v3 中最容易混淆的两个配置是 staleTimecacheTime。它们都以毫秒为单位,但解决的不是同一件事:

参数回答的问题影响的行为
staleTime这份数据在多长时间内仍可被认为是新鲜的数据何时从 fresh 变为 stale,以及重新挂载、窗口聚焦等场景是否需要后台刷新
cacheTime页面不再使用这份数据以后,缓存还保留多久无活跃订阅的查询何时从内存中被垃圾回收

这里以项目中使用的 React Query v3 为准,所以使用 cacheTime 这个参数名。后续 TanStack Query v5 将它改名为 gcTime,正是为了避免误解:它不是“数据新鲜多久”,而是查询进入不活跃状态以后“缓存多久再回收”。

React Query v3 的默认值可以简单记为:

staleTime: 0cacheTime: 5 * 60 * 1000

也就是说,接口成功返回的数据会立刻被视为 stale,但组件卸载以后仍可以在缓存中保留 5 分钟。stale 不等于“没有缓存”,也不等于页面立即清空旧数据;它表示在满足重新拉取触发条件时,可以在展示缓存数据的同时发起后台请求。

staleTime 决定多久需要再次确认服务端事实

假设用户先进入项目详情页,获取了一份项目数据,然后返回列表并很快再次进入同一个详情页:

useQuery({  queryKey: queryKeys.projectDetail(projectId),  queryFn: () => projectService.fetchDetail(projectId),  staleTime: 30 * 1000});

如果第二次进入发生在 30 秒内,缓存中的详情数据仍属于 fresh,通常可以直接使用,不需要因为组件重新挂载而立即再次请求。如果超过 30 秒,数据已经 stale,详情页仍然能够先读到缓存内容,但 React Query 可以在后台重新拉取最新结果。

在复杂项目工具中,staleTime 的设置取决于数据变化速度和用户对于实时性的要求:

数据类型可考虑的 staleTime原因
字典、项目类型、权限枚举5 - 30 分钟内容很少变化,重复请求价值较低
项目列表、项目详情30 秒至 2 分钟需要兼顾页面切换速度和他人修改后的可见性
任务状态、审批状态0 - 30变化相对频繁,需要较快重新确认
正在协作的实时看板较短或配合推送/轮询仅靠较长缓存时间无法保证及时变化

时间越长,请求次数越少,页面往返体验越轻;但其他用户已经改变的数据,也会更久以后才被主动重新确认。staleTime 因此是“减少无效刷新”与“及时感知变化”之间的取舍。

cacheTime 决定离开页面以后还能不能快速回来

cacheTime 只在一个查询没有任何活跃使用者以后开始计时。例如用户从项目详情返回项目列表,详情查询不再被组件订阅,才会进入 inactive 状态并开始等待回收:

useQuery({  queryKey: queryKeys.projectDetail(projectId),  queryFn: () => projectService.fetchDetail(projectId),  staleTime: 30 * 1000,  cacheTime: 10 * 60 * 1000});

在这 10 分钟内,用户再次进入同一个项目详情,即使数据已经过了 30 秒的新鲜期,页面也能优先展示缓存中的详情,再在后台请求最新数据。这种“先显示已有内容,再校正结果”的体验,对于频繁在列表和详情之间跳转的后台系统很重要。

如果缓存已经被回收,用户再次进入详情页时就没有可复用数据,页面需要等待新的请求完成后才能完整展示。

可以将一个查询的生命周期理解为:

首次进入页面  -> 请求完成并写入缓存  -> staleTime 内数据为 fresh  -> 超过 staleTime 后数据为 stale,但缓存仍可展示  -> 页面离开且无人使用查询,开始计算 cacheTime  -> cacheTime 到期后缓存被回收

因此,staleTimecacheTime 可以存在以下组合:

配置组合用户体验适合场景
较短 staleTime + 较长 cacheTime返回页面立即有旧数据,同时较积极刷新列表、详情、协作型业务
较长 staleTime + 较长 cacheTime页面切换快,请求少稳定字典和低频变化配置
较短 staleTime + 较短 cacheTime更容易重新 loading,请求更多数据敏感且不应长期保留的场景

在多数 To B 页面中,比较常见的选择是让 cacheTime 明显长于 staleTime:允许页面短时间返回时快速看到已有结果,同时仍然按照业务需要刷新服务端数据。

数据新鲜度需要结合页面性质决定

To B 页面经常被长时间打开。用户停留十分钟后重新切回标签页,数据可能已经由其他操作改变。可以按页面特点配置刷新策略:

useQuery({  queryKey: queryKeys.projectDetail(projectId),  queryFn: () => projectService.fetchDetail(projectId),  staleTime: 30 * 1000,  cacheTime: 10 * 60 * 1000,  refetchOnWindowFocus: true});

这里表示详情数据 30 秒内可直接认为新鲜;用户离开该详情页面后,它还可在缓存中保留 10 分钟;页面重新获得焦点时,如果数据已经过期,则可以请求最新结果。

缓存与编辑状态仍然需要隔离。正在编辑的表单不能在后台刷新后直接覆盖用户输入。比较安全的方式是保持已加载版本作为编辑基线,发现服务端已有变化时提示用户重新确认,而不是静默替换草稿。

不要只靠时间参数处理数据更新

staleTime 决定的是自动重新确认的节奏,不能替代一次明确业务操作后的缓存同步。例如用户在详情页完成任务状态更新,这时不应等待 30 秒以后缓存自然过期,而应立即更新或失效相关查询:

queryClient.invalidateQueries({  queryKey: queryKeys.projectDetail(projectId)});queryClient.invalidateQueries({  queryKey: queryKeys.tasks(projectId)});queryClient.invalidateQueries({  queryKey: ["projects", "list"]});

同样,cacheTime 也不代表数据在缓存存活期间一定可信。它只决定有没有一份可用于快速展示的历史结果;历史结果是否还新鲜,仍由 staleTime、主动失效和刷新策略共同判断。

复杂项目中的一致性来自清晰规则

React Query 降低了请求缓存与状态反馈的重复工作,但数据一致性仍来自以下约定:

  1. 明确哪些信息属于服务端状态。
  2. 为列表、详情和关联资源设计稳定查询键。
  3. 修改成功后明确哪些缓存需要失效或局部更新。
  4. 对长时间打开和多人变化的数据制定刷新策略。
  5. staleTimecacheTime 分别确定数据新鲜期和缓存保留期。
  6. 对编辑场景避免后台数据无提示覆盖本地输入。

在复杂项目管理工具中,页面数量不断增加并不可怕;真正危险的是同一份数据在各处呈现出不同事实。缓存策略写清楚以后,前端才有能力在体验速度和数据可靠之间取得平衡。

参考资料


Share this post on: