TanStack Query ESLint 规则实战:no-rest-destructuring 如何从源头避免不必要的重渲染
本文围绕 TanStack Query 仓库中官方 ESLint 插件(@tanstack/eslint-plugin-query)的 no-rest-destructuring 规则展开,讲清楚它对"对 query 结果做 rest 解构/展开"这类写法的检测逻辑、命中的所有场景(含 useQueries 数组解构、变量二次展开、类型感知的自定义 Hook 识别),以及它在 recommended 配置中的等级。读完后,你能在自己的项目里正确启用该规则,并理解它背后 TanStack Query 基于属性追踪(tracked queries)的按需重渲染机制。
规则要解决的问题:rest 解构会让"按需订阅"失效
TanStack Query v5 默认启用了属性追踪:组件对 query 结果对象的哪些属性进行了实际读取,就只订阅这些属性的变化。这一机制的开关就在 useBaseQuery 的返回值处理中——当没有手动设置 notifyOnChangeProps 时,返回的是被 Proxy 包装、可追踪属性访问的结果对象,否则原样返回(见 useBaseQuery.ts):
// Handle result property usage tracking
return !defaultedOptions.notifyOnChangeProps
? observer.trackResult(result)
: result
追踪的核心实现在 QueryObserver 中:trackResult 返回的 Proxy 在每次属性 get 时把该属性名记入 #trackedProps,之后比较两次结果是否变化时,若没有手动 notifyOnChangeProps,就以 #trackedProps 为比较范围(见 queryObserver.ts 中的 trackResult 与 #trackedProps)。
问题在于:const { data, ...rest } = useQuery(...) 这种 rest 解构会把结果对象上所有尚未被显式列出的属性都"访问"一遍。对 Proxy 来说,这意味着全部属性都被记入了 #trackedProps——组件相当于订阅了 query 结果的每一个字段,任何字段变化(包括你不关心的 isFetched、updateTimestamp 等)都会触发不必要的重渲染。no-rest-destructuring 规则就是为了在编码期拦住这类写法,确保"只订阅真正用到的字段"。
规则细节:错误与正确写法
以下示例继承自官方规则文档 no-rest-destructuring.md。
错误写法(会触发 warn):
/* eslint "@tanstack/query/no-rest-destructuring": "warn" */
const useTodos = () => {
const { data: todos, ...rest } = useQuery({
queryKey: ['todos'],
queryFn: () => api.getTodos(),
})
return { todos, ...rest }
}
正确写法:普通解构只取需要的字段,或保留整个结果变量供按属性读取:
const todosQuery = useQuery({
queryKey: ['todos'],
queryFn: () => api.getTodos(),
})
// normal object destructuring is fine
const { data: todos } = todosQuery
规则元信息(见 规则实现):
| 属性 | 值 | 说明 |
|---|---|---|
| type | problem |
属于"问题类"规则,而非风格类 |
| recommended | warn |
被收录在 recommended 预设中,默认等级为 warn |
| schema | [] |
规则不接受任何选项配置 |
| Fixable | 否 | 无自动修复器,需手动改写 |
| Fixable / 文档标注 | Recommended ✅,Fixable 否 | 与 规则文档 的 Attributes 一致 |
从源码看:规则到底检查哪些 AST 节点
规则入口通过 detectTanstackQueryImports 包装器(见 detect-react-query-imports.ts)注入,该包装器会先扫描 ImportDeclaration,只把来源以 @tanstack/ 开头且以 -query 结尾(即 @tanstack/react-query、@tanstack/solid-query、@tanstack/vue-query 等)的具名导入登记为"真·TanStack Query 导入"。这意味着从其他包导入的同名 useQuery 不会被误报——测试用例中专门覆盖了 import { useQuery } from 'other-package' 的合法场景(见 测试文件)。
在导入确认的前提下,规则监听三类 AST 节点,覆盖四种实际场景:
场景一:直接对 query 结果做 rest 解构
CallExpression 处理器判断被调用的 Hook 是否属于以下六个(见 规则源码 L10-L17):
useQueryuseInfiniteQueryuseQueriesuseSuspenseQueryuseSuspenseInfiniteQueryuseSuspenseQueries
若调用语句的父节点是 VariableDeclarator,且绑定目标是带 RestElement 的 ObjectPattern(由 NoRestDestructuringUtils.isObjectRestDestructuring 判定,即 const { data, ...rest } = ...),直接报告错误:
const { data, ...rest } = useQuery({ queryKey: ['todos'], queryFn: () => api.getTodos() })
场景二:useQueries / useSuspenseQueries 的数组元素
useQueries 返回的是数组,规则要求解构目标为 ArrayPattern,并逐个检查元素:哪个元素是带 rest 的对象解构,就只报告那个元素(见 规则源码 L92-L109):
const [query1, { data, ...rest }] = useQueries([
{ queryKey: ['key1'], queryFn: () => {} },
{ queryKey: ['key2'], queryFn: () => {} },
])
注意数组里的 rest(如 [..., ...others])不算问题,只有对象元素中的 ...rest 才会触发;对应合法用例见 测试 L104-L119。
场景三:先存变量,之后 rest 解构或展开
这是很多团队最常见的"偷懒"写法。规则会把"接收 query 结果的变量名"记入 queryResultVariables 集合,然后用两个后续监听器兜底:
VariableDeclarator:若某条const { data, ...rest } = todosQuery的右侧标识符在集合中,报告错误;SpreadElement:若对象表达式中...todosQuery展开了集合中的变量,报告错误。
对应测试用例(测试 L375-L398):
const query = useQuery()
return { ...query, data: query.data[0] } // 报错:展开即等价于 rest 解构
从源码结构看,这条链路是纯词法追踪(变量名集合),不跨作用域做数据流分析,因此它只覆盖"同一文件内直接赋值—使用"的简单情形。
类型感知路径:识别"返回 query 结果的自定义 Hook"
官方文档指出:启用类型感知 lint(typed linting)后,该规则还能命中返回 TanStack Query 结果的自定义 Hook。这条路径的实现分两步(见 no-rest-destructuring.utils.ts):
-
判断调用是否"返回 query 结果":
isQueryResultCall借助parserServices.program.getTypeChecker()取出被调用位置的调用签名返回类型,判断其(或联合类型的任一成员)的符号名是否属于以下白名单:UseBaseQueryResult / UseQueryResult / UseSuspenseQueryResult / DefinedUseQueryResult / UseInfiniteQueryResult / UseSuspenseInfiniteQueryResult / DefinedUseInfiniteQueryResult / QueryObserverResult / InfiniteQueryObserverResult因此 Hook 直接透传
useQuery返回值、或显式标注返回QueryObserverResult都能被识别(对应 类型感知测试用例)。 -
限制昂贵的类型查询:只有当绑定目标本身是
Identifier或已经是 rest 解构时才执行类型查询(见 规则源码 L51-L68 的注释:"The type-aware path can report when the result is rest destructured or assigned to an identifier that may later be spread")。
类型感知模式下被命中的写法示例(均出自 测试 L440-L504):
const useTodos = () =>
useQuery({ queryKey: ['todos'], queryFn: () => Promise.resolve([]) })
function Component() {
const { data, ...rest } = useTodos() // 报错
// const q = useTodos(); return { ...q } // 报错
// const q = useTodos(); const { data, ...r } = q // 报错
}
同时该模式不会误报:返回普通对象({ data: 1, isError: false })的同名 Hook 不报错;自定义 Hook 无 rest 解构时也不报错。
要启用这条路径,需要在 ESLint 中使用支持类型信息的 parser 选项(@typescript-eslint/parser 且开启 project/tsconfig 关联);类型感知测试就是这么配置的(测试 L402-L410)。从 插件 package.json 可见,typescript 是可选的 peer dependency("typescript": "^5.6.0 || ^6.0.0 || ^7.0.0"),也就是说不开类型感知时插件完全可用,只是少了对自定义 Hook 的覆盖。
启用方式与推荐配置
插件在 index.ts 中把本规则写入了 recommended 预设,等级为 warn(recommended-strict 沿用同一等级):
const recommendedRules = {
'@tanstack/query/exhaustive-deps': 'error',
'@tanstack/query/no-rest-destructuring': 'warn',
// ...其余规则
}
插件同时提供 eslintrc 风格的 recommended 与 flat config 风格的 flat/recommended(见 configs 定义),peer 依赖支持 ESLint ^8.57.0 || ^9.0.0 || ^10.0.0。按当前仓库的 peer 声明,典型启用方式为安装插件并引用预设(此处仅说明配置方式,不涉及修改本仓库):
// eslint.config.js(flat config)
import query from '@tanstack/eslint-plugin-query/config/flat/recommended'
export default [
query, // 已包含 '@tanstack/query/no-rest-destructuring': 'warn'
]
// eslint 单独调整该规则等级
{
"rules": {
"@tanstack/query/no-rest-destructuring": "error"
}
}
如果想同时享受自定义 Hook 的检测能力,请在 parser 选项中启用类型感知(parserOptions.project 指向项目 tsconfig),前提是使用 @typescript-eslint/parser 且项目内已安装 TypeScript。
什么时候可以关闭这条规则
规则文档的 When Not To Use It 说明:如果你手动设置了 notifyOnChangeProps,可以禁用这条规则。原因回到开头的机制:一旦显式提供 notifyOnChangeProps,useBaseQuery 会跳过 trackResult 的 Proxy 包装、原样返回结果(useBaseQuery.ts L140-L142),组件改为由你指定的属性列表触发重渲染,此时 rest 解构本身不再经由"全量访问 Proxy"造成追踪失效——代价是你需要自行维护这份属性清单。
小结
no-rest-destructuring是 TanStack Query 官方 ESLint 插件中唯一以warn收录在 recommended 预设里的"性能提示型"规则,核心目的是保护 v5 属性追踪带来的按需重渲染收益。- 它无配置项、无自动修复,检测面覆盖 6 个 query 类 Hook 的直接 rest 解构、
useQueries数组元素 rest 解构、以及"存变量后二次 rest 解构/展开"三种 AST 形态。 - 开启类型感知后,可通过返回类型白名单(
UseQueryResult、QueryObserverResult等 9 种类型)识别返回 query 结果的自定义 Hook,把检测延伸到业务代码的 Hook 层。 - 若项目全局手动管理
notifyOnChangeProps,可按官方文档关闭该规则,重渲染粒度由属性清单接管。 - 全部检测逻辑与正反用例可在 规则实现、类型工具 与 测试文件 中逐条对照验证。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00