首页
/ TanStack Query ESLint 规则详解:prefer-query-options 如何强制 queryKey 与 queryFn 同处定义

TanStack Query ESLint 规则详解:prefer-query-options 如何强制 queryKey 与 queryFn 同处定义

2026-09-05 17:19:44作者:宣利权Counsellor

在 TanStack Query 生态中,queryKeyqueryFn 分离书写是常见的历史写法,但同一 key 被不同 fetch 函数复用时会引发难以察觉的运行时问题。本文基于仓库中 prefer-query-options 规则文档 展开,结合 eslint-plugin-query 规则实现源码测试用例,完整讲解该规则的检查范围、违规与修复写法、queryKey 复用检查以及底层 AST 判定原理。读完你可以将该规则接入项目、理解它覆盖哪些 Hook 与 QueryClient 方法,并判断它是否适合你的代码库。

规则动机:为什么 queryKey 和 queryFn 应该放在一起

规则文档开宗明义:把 queryKeyqueryFn 分开写在 Hook 参数里,当同一个 query key 被意外地与多个不同的 queryFn 一起使用时,会导致不可预期的运行时行为——例如某个组件用 key ['get', id] 取到了另一个组件 queryFn 写入的缓存数据。用 queryOptions(无限滚动场景用 infiniteQueryOptions)把 key 和函数包裹在一起,可以让二者在物理位置上绑定(co-locate),查询因此更安全、也更容易被多个组件复用。

规则元数据在源码中被声明为:

// packages/eslint-plugin-query/src/rules/prefer-query-options/prefer-query-options.rule.ts
meta: {
  type: 'problem',
  docs: {
    description:
      'Prefer using queryOptions() to co-locate queryKey and queryFn',
    recommended: 'strict',
  },
  messages: {
    preferQueryOptions:
      'Prefer using queryOptions() or infiniteQueryOptions() to co-locate queryKey and queryFn.',
    preferQueryOptionsQueryKey:
      'Prefer referencing a queryKey from a queryOptions() result instead of typing it manually.',
  },
  schema: [],
}

两个关键信息:

  • 规则类型为 problem,且 recommended: 'strict',即它只在 recommended-strict 预设中默认开启,而不是普通 recommended 预设;
  • schema: [] 表示该规则不接受任何选项配置,开启即全量生效。

在插件入口 index.ts 中,预设的差异也印证了这一点:recommendedRules 里不包含该规则,只有 recommendedStrictRules 显式追加了 '@tanstack/query/prefer-query-options': 'error',并且对应的 flat/recommended-strict Flat Config 同步暴露给 Flat Config 用户。

违规写法与正确写法

在查询 Hook 中内联 queryKey / queryFn(违规)

文档给出的两类错误示例如下。第一类是直接在组件里写:

/* eslint "@tanstack/query/prefer-query-options": "error" */

function Component({ id }) {
  const query = useQuery({
    queryKey: ['get', id],
    queryFn: () => Api.get(`/foo/${id}`),
  })
  // ...
}

第二类是把参数化选项封装成自定义 Hook,仍然违规:

/* eslint "@tanstack/query/prefer-query-options": "error" */

function useFooQuery(id) {
  return useQuery({
    queryKey: ['get', id],
    queryFn: () => Api.get(`/foo/${id}`),
  })
}

修复方式是引入 queryOptions 构建函数,让 key 与 fetch 逻辑定义在一起,Hook 只消费其结果:

/* eslint "@tanstack/query/prefer-query-options": "error" */

function getFooOptions(id) {
  return queryOptions({
    queryKey: ['get', id],
    queryFn: () => Api.get(`/foo/${id}`),
  })
}

function Component({ id }) {
  const query = useQuery(getFooOptions(id))
  // ...
}

也允许在消费侧展开结果并追加选项(如 select),同样不报错:

/* eslint "@tanstack/query/prefer-query-options": "error" */

function getFooOptions(id) {
  return queryOptions({
    queryKey: ['get', id],
    queryFn: () => Api.get(`/foo/${id}`),
  })
}

function useFooQuery(id) {
  return useQuery({ ...getFooOptions(id), select: (data) => data.foo })
}

这里值得注意的是:文档示例中 Component 直接传入 useQuery(getFooOptions(id)) 这类函数调用结果也能通过检查。这一点在测试文件 prefer-query-options.test.ts 的 "hooks consuming queryOptions result" 分组中被逐条验证,包括传入 queryOptions 常量、传入函数调用结果、展开后追加 select 等合法形态。

QueryClient 方法中手写 queryKey(违规)

规则的第二重检查针对 QueryClient 的方法与过滤器:应当复用 queryOptions 结果上的 queryKey,而不是在调用处再次手写一遍数组。错误示例:

/* eslint "@tanstack/query/prefer-query-options": "error" */

function todoOptions(id) {
  return queryOptions({
    queryKey: ['todo', id],
    queryFn: () => api.getTodo(id),
  })
}

function Component({ id }) {
  const queryClient = useQueryClient()
  return queryClient.getQueryData(['todo', id])
}
/* eslint "@tanstack/query/prefer-query-options": "error" */

function todoOptions(id) {
  return queryOptions({
    queryKey: ['todo', id],
    queryFn: () => api.getTodo(id),
  })
}

function Component({ id }) {
  const queryClient = useQueryClient()
  return queryClient.invalidateQueries({ queryKey: ['todo', id] })
}

正确的写法是引用 options 结果上的 queryKey 属性:

/* eslint "@tanstack/query/prefer-query-options": "error" */

function todoOptions(id) {
  return queryOptions({
    queryKey: ['todo', id],
    queryFn: () => api.getTodo(id),
  })
}

function Component({ id }) {
  const queryClient = useQueryClient()
  return queryClient.getQueryData(todoOptions(id).queryKey)
}
/* eslint "@tanstack/query/prefer-query-options": "error" */

function todoOptions(id) {
  return queryOptions({
    queryKey: ['todo', id],
    queryFn: () => api.getTodo(id),
  })
}

function Component({ id }) {
  const queryClient = useQueryClient()
  return queryClient.invalidateQueries({ queryKey: todoOptions(id).queryKey })
}

这样 key 只有一个事实来源:改 todoOptions 里的 key 时,所有 QueryClient 调用点自动跟随,不会出现"改了定义处、漏改 invalidate 处"这类 key 漂移。

规则的检查范围:从源码看它覆盖了哪些调用点

规则文档只展示了 useQueryQueryClient 的部分方法,而 规则源码 顶部的常量数组完整列出了检查面,可以据此判断哪些写法会被扫到:

  • 直接触发 preferQueryOptions 报告的查询 Hook(queryHooks):useQueryuseInfiniteQueryuseSuspenseQueryuseSuspenseInfiniteQueryusePrefetchQueryusePrefetchInfiniteQuery
  • 批量查询 Hook(queriesHooks):useQueriesuseSuspenseQueries——规则会读取 queries 数组中的每个 query 对象(支持数组字面量与 .map(...) 映射),逐一检查是否内联了 queryKeyqueryFn
  • 过滤器 Hook(filterHooks):useIsFetching,其选项对象中出现内联 queryKey 数组时报告 preferQueryOptionsQueryKey
  • QueryClient 选项类方法(queryClientOptionMethods):fetchQueryprefetchQueryfetchInfiniteQueryprefetchInfiniteQueryensureQueryDataensureInfiniteQueryData,参数对象内联 queryKey/queryFn 时报 preferQueryOptions
  • QueryClient 纯 key 类方法(queryClientQueryKeyMethods):getQueryDatasetQueryDatagetQueryStatesetQueryDefaultsgetQueryDefaults,第一个参数是内联数组字面量时直接报 preferQueryOptionsQueryKey
  • QueryClient 过滤器类方法(queryClientFilterMethods):invalidateQueriescancelQueriesrefetchQueriesremoveQueriesresetQueriesisFetchinggetQueriesDatasetQueriesData,其 queryKey 字段为内联数组时报 preferQueryOptionsQueryKey
  • queryOptionsBuildersqueryOptionsinfiniteQueryOptions 的调用本身永远放行。

此外,判定"参数对象是否内联"的逻辑很宽松:只要对象里同时或单独出现 queryKeyqueryFn 任一属性就报告(见 hasInlineQueryOptions 函数),测试用例也验证了仅写 useQuery({ queryKey: ['todos'] }) 而不写 queryFn 同样违规。而 useQuery(todosOptions)useQuery({ ...todosOptions, select: ... })invalidateQueries({ queryKey: todosOptions.queryKey, exact: true }) 等形态均被测试确认为合法。

识别机制:只有 @tanstack/*-query 的导入才会被检查

该规则不会盲扫所有 useQuery 调用。它由通用包装器 detectTanstackQueryImports 包裹:

  • 只有当 ImportDeclaration 的模块名 startsWith('@tanstack/')endsWith('-query')(如 @tanstack/react-query@tanstack/vue-query@tanstack/solid-query 等)时,该声明的所有具名导入才会进入识别集合;
  • 规则内部再通过 getTanstackImportName 用作用域解析把调用点标识符映射回导入的原始名称(imported.name),因此重命名导入(import { useQuery as useQ })也能被正确识别;
  • QueryClient 方法,isTanstackQueryClient 还会解析调用对象的来源:来自 useQueryClient()new QueryClient() 的实例才被检查,测试用例专门验证了"被局部参数遮蔽的 queryClient"和"非 TanStack 对象的 fetchQuery"均被忽略。

测试文件中还有一条明确的边界用例:从 other-library 导入的同名 useQuery 完全不受该规则影响。这一点在跨框架使用(如项目中同时存在自研数据层)时很重要——规则的作用域严格限定于 TanStack Query 官方包。

如何接入:配置与适用前提

在 Flat Config 项目中,推荐直接复用插件内置预设。由于该规则属于 strict 档,需要选择 flat/recommended-strict 预设(或在 flat/recommended 基础上手动加一条规则):

import query from '@tanstack/eslint-plugin-query'

export default [
  {
    plugins: {
      '@tanstack/query': query,
    },
    rules: {
      '@tanstack/query/prefer-query-options': 'error',
    },
  },
]

在 Legacy(eslintrc)配置中则对应 query.configs.recommendedStrict

适用前提与限制:

  • 该规则依赖运行 ESLint 的文件中显式存在来自 @tanstack/*-query 包的 import 语句,通过 import 识别确定作用域;
  • 规则不接受选项(schema: []),无法按文件粒度豁免某个 Hook 时只能借助 // eslint-disable 注释或配置块级覆盖;
  • type: 'problem' 与文档的 "When Not To Use It" 部分看,如果你不希望代码库强制 queryOptions 风格(例如大量既有代码内联选项、迁移成本过高),就不需要开启这条规则;
  • 文档 Attributes 标注:✅ Recommended (strict) 勾选、🔧 Fixable 未勾选,即当前没有自动修复能力,报告后需要手动将内联选项抽取到 queryOptions/infiniteQueryOptions 中。

小结

prefer-query-options 是 TanStack Query ESLint 插件 strict 预设中最具架构导向的一条规则:它通过 preferQueryOptionspreferQueryOptionsQueryKey 两个报告消息,把"key 与 fetch 函数同处定义"和"key 单一事实来源"变成可静态检查的约束。其检查面覆盖全部查询/预取 Hook、批量 Hook、useIsFetching 过滤器以及 QueryClient 的选项类、key 类、过滤器类方法,识别机制严格限定在 @tanstack/*-query 导入之上。对于以 queryOptions 为中心组织数据层的项目,开启它能在代码评审之前就把 key 漂移这类运行时隐患挡在 lint 阶段。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.83 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.79 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
988
506
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384