首页
/ TanStack Query ESLint 规则实战:no-rest-destructuring 如何从源头避免不必要的重渲染

TanStack Query ESLint 规则实战:no-rest-destructuring 如何从源头避免不必要的重渲染

2026-09-05 23:12:01作者:廉皓灿Ida

本文围绕 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 结果的每一个字段,任何字段变化(包括你不关心的 isFetchedupdateTimestamp 等)都会触发不必要的重渲染。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):

  • useQuery
  • useInfiniteQuery
  • useQueries
  • useSuspenseQuery
  • useSuspenseInfiniteQuery
  • useSuspenseQueries

若调用语句的父节点是 VariableDeclarator,且绑定目标是带 RestElementObjectPattern(由 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):

  1. 判断调用是否"返回 query 结果"isQueryResultCall 借助 parserServices.program.getTypeChecker() 取出被调用位置的调用签名返回类型,判断其(或联合类型的任一成员)的符号名是否属于以下白名单:

    UseBaseQueryResult / UseQueryResult / UseSuspenseQueryResult /
    DefinedUseQueryResult / UseInfiniteQueryResult /
    UseSuspenseInfiniteQueryResult / DefinedUseInfiniteQueryResult /
    QueryObserverResult / InfiniteQueryObserverResult
    

    因此 Hook 直接透传 useQuery 返回值、或显式标注返回 QueryObserverResult 都能被识别(对应 类型感知测试用例)。

  2. 限制昂贵的类型查询:只有当绑定目标本身是 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 预设,等级为 warnrecommended-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,可以禁用这条规则。原因回到开头的机制:一旦显式提供 notifyOnChangePropsuseBaseQuery 会跳过 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 形态。
  • 开启类型感知后,可通过返回类型白名单(UseQueryResultQueryObserverResult 等 9 种类型)识别返回 query 结果的自定义 Hook,把检测延伸到业务代码的 Hook 层。
  • 若项目全局手动管理 notifyOnChangeProps,可按官方文档关闭该规则,重渲染粒度由属性清单接管。
  • 全部检测逻辑与正反用例可在 规则实现类型工具测试文件 中逐条对照验证。
登录后查看全文
热门项目推荐
相关项目推荐