TanStack Query stable-query-client 规则详解:确保 QueryClient 实例稳定,避免每次渲染丢失缓存
【免费下载链接】query🤖 Powerful asynchronous state management, server-state utilities and data fetching for the web. TS/JS, React Query, Solid Query, Svelte Query and Vue Query.项目地址: https://gitcode.com/GitHub_Trending/qu/query
本文围绕 TanStack Query 官方 ESLint 插件中的stable-query-client规则展开,讲解它要解决的真实问题——QueryClient 在组件渲染过程中被反复重建导致的缓存丢失;给出官方文档中的正确与错误写法对照,并结合插件源码 stable-query-client.rule.ts、导入检测工具 detect-react-query-imports.ts 以及测试用例 stable-query-client.test.ts,完整还原该规则的触发条件、豁免逻辑与自动修复行为,帮助你在项目中正确配置并理解这条推荐级规则。
为什么 QueryClient 必须稳定
规则文档的核心论点是:QueryClient 内部持有 QueryCache,因此整个应用生命周期内只应创建一个实例,而不能在每次渲染时都新建一个。这一论断可以直接在核心包源码中得到印证。QueryClient 类定义 显示:
export class QueryClient { #queryCache: QueryCache #mutationCache: MutationCache #defaultOptions: DefaultOptions #queryDefaults: Map<string, QueryDefaults> #mutationDefaults: Map<string, MutationDefaults> #mountCount: number // ... constructor(config: QueryClientConfig = {}) { this.#queryCache = config.queryCache || new QueryCache() this.#mutationCache = config.mutationCache || new MutationCache() // ... } }如果不在配置中显式传入queryCache,每次new QueryClient()都会构造一个全新的空 QueryCache(以及空的 MutationCache)。也就是说,当你在组件函数体里直接new QueryClient()时,组件每重渲染一次,之前所有的查询数据、请求状态就会随着旧实例一起被丢弃,界面表现为数据反复闪烁、请求重复发出。这正是stable-query-client规则要拦截的「problem」类错误——插件元信息中将其标记为type: 'problem',描述为 "Makes sure that QueryClient is stable",并且在推荐配置中默认以error级别启用(见 插件入口 index.ts)。
规则属性与基本行为
stable-query-client规则的两个关键属性:
| 属性 | 说明 |
|---|---|
| 推荐级别 | 属于recommended配置,默认以error级别开启 |
| 可自动修复 | 是(fixable: 'code'),可自动改写为React.useState写法 |
| 规则选项 | 无(schema: [],defaultOptions: []),开启即生效 |
报告的错误信息(规则源码 messages.unstable)为:
QueryClient is not stable. It should be either extracted from the component or wrapped in React.useState.
即修复方向只有两条:把QueryClient提取到组件外部(模块作用域),或者用React.useState包裹构造过程。
错误示例
下面的写法会触发该规则:new QueryClient()位于组件函数体内部,每次渲染都会执行,生成新的实例。
/* eslint "@tanstack/query/stable-query-client": "error" */ function App() { const queryClient = new QueryClient() return ( <QueryClientProvider client={queryClient}> <Home /> </QueryClientProvider> ) }正确示例
官方文档给出了三种合规写法,分别对应「useState 惰性初始化」「模块级单例」「服务端异步组件」三个场景:
// 方式一:useState 惰性初始化(规则自动修复后的标准形态) function App() { const [queryClient] = useState(() => new QueryClient()) return ( <QueryClientProvider client={queryClient}> <Home /> </QueryClientProvider> ) }// 方式二:模块作用域创建,应用生命周期内唯一实例 const queryClient = new QueryClient() function App() { return ( <QueryClientProvider client={queryClient}> <Home /> </QueryClientProvider> ) }// 方式三(例外情况):异步 Server Component 内创建 async function App() { const queryClient = new QueryClient() await queryClient.query(options) }文档特别强调的例外是:在异步 Server Component中允许创建新的 QueryClient,因为该 async 函数在服务端只会被调用一次,不存在「每次渲染重建」的问题。这一点在规则实现中被显式处理(下文详述),并有对应测试用例覆盖。
规则实现解析:什么情况下会触发报告
规则的完整实现位于 stable-query-client.rule.ts。它监听 AST 中的每一个NewExpression节点,需要同时满足以下条件才会报告错误:
- 构造的必须是
QueryClient:node.callee必须是一个名为QueryClient的标识符; - 必须直接来自
@tanstack/react-query:通过导入检测工具确认该标识符确实从@tanstack/react-query导入; - 必须位于变量声明中:
NewExpression的父节点是VariableDeclarator; - 所在函数是 React 组件或 Hook:最近的函数祖先的名字符合 React 组件(大写开头)或 Hook(
use前缀)的命名约定; - 所在函数不是 async 函数:async 函数被视为 Server Component,予以豁免。
只针对 @tanstack/react-query 的导入
规则通过 detectTanstackQueryImports 高阶函数增强。该工具在文件解析初期收集所有ImportDeclaration,只保留满足「模块名以@tanstack/开头、以-query结尾」的导入(检测逻辑);随后规则用isSpecificTanstackQueryImport(node.callee, '@tanstack/react-query')精确比对来源模块。
这一设计的实际效果在测试用例中得到清晰体现(测试文件 valid 部分):
QueryClient从"other-library"导入 —— 不报告(不是 TanStack Query 生态的标识符);QueryClient从@tanstack/solid-query导入 —— 不报告(该规则只对 React 版负责,因为规则针对的是 React 渲染循环带来的实例不稳定问题)。
组件/Hook 命名约定与 async 豁免
判断「所在函数是否是 React 组件或 Hook」依赖 ASTUtils.isValidReactComponentOrHookName,其实现就是一个正则:/^(use|[A-Z])/,即函数名以use开头或大写字母开头。而 getFunctionAncestor 负责向上遍历祖先节点,找到第一个FunctionDeclaration、FunctionExpression或ArrowFunctionExpression。
由此可以推断出规则的完整判定边界:
new QueryClient()写在一个普通函数(如function someFn())里 ——不报告,因为它不是组件/Hook,不处于渲染循环中;- 写在模块顶层 ——不报告,没有函数祖先;
- 写在
async函数里 ——不报告,规则源码 中isReactServerComponent = fnAncestor?.async === true直接豁免,这正是文档中「异步 Server Component 例外」的实现依据。
自动修复:改写为 React.useState
当违规代码是const xxx = new QueryClient(...)形式(变量名为普通标识符)时,规则提供代码级自动修复(fixer 实现):将整个变量声明替换为[xxx] = React.useState(() => new QueryClient(...)),并完整保留原有的构造参数。测试用例验证了修复的四个关键细节:
// 1. 组件内的直接构造 —— 报告并修复 function Component() { const queryClient = new QueryClient() } // 自动修复为: // const [queryClient] = React.useState(() => new QueryClient()) // 2. 自定义 Hook 内同样适用 function useHook() { const queryClient = new QueryClient() } // 自动修复为: // const [queryClient] = React.useState(() => new QueryClient()) // 3. 构造参数原样保留 const queryClient = new QueryClient({ defaultOptions: { /* ... */ } }) // 修复为: // const [queryClient] = React.useState(() => new QueryClient({ defaultOptions: { /* ... */ } })) // 4. 变量名保持原样 const customName = new QueryClient() // 修复为: // const [customName] = React.useState(() => new QueryClient())需要注意的是修复能力的边界:当变量声明是解构形式(如const { defaultOptions } = new QueryClient())时,由于parent.id.type不是Identifier,fixer 提前返回,只报告错误而不自动修复(测试用例 "QueryClient with destructuring pattern reports error without autofix" 中output: null明确验证了这一点)。此类写法仍需手工改为模块级常量或useState形式。
从测试用例还能看出,规则对「稳定写法」的认定比文档示例更宽泛:React.useState、useState、React.useMemo(() => new QueryClient(), []),甚至任意名为useAnything的自定义 Hook 包裹的构造,都因不满足「组件内直接new QueryClient」的模式而不被报告。从源码结构看,这是因为规则只匹配「NewExpression 直接作为 VariableDeclarator 的 init」这一形态,被函数调用包裹的构造天然不在监听范围内。
在项目中使用该规则
安装
插件是独立发布的包(package.json 中版本为 5.102.8,peer 依赖eslint ^8.57.0 || ^9.0.0 || ^10.0.0,可选 peer 依赖typescript ^5.6.0 || ^6.0.0 || ^7.0.0):
npm i -D @tanstack/eslint-plugin-query或使用 pnpm / yarn / bun:
pnpm add -D @tanstack/eslint-plugin-query yarn add -D @tanstack/eslint-plugin-query bun add -D @tanstack/eslint-plugin-queryFlat Config(eslint.config.js)
推荐直接使用插件自带的 preset(插件 configs 定义),flat/recommended中即包含'@tanstack/query/stable-query-client': 'error':
import pluginQuery from '@tanstack/eslint-plugin-query' export default [ ...pluginQuery.configs['flat/recommended'], // Any other config... ]如需更严格的约束,可以使用flat/recommended-strict(在 recommended 基础上额外启用prefer-query-options):
import pluginQuery from '@tanstack/eslint-plugin-query' export default [ ...pluginQuery.configs['flat/recommended-strict'], // Any other config... ]当然也可以只加载本规则,按需配置级别:
import pluginQuery from '@tanstack/eslint-plugin-query' export default [ { plugins: { '@tanstack/query': pluginQuery, }, rules: { '@tanstack/query/stable-query-client': 'error', }, }, // Any other config... ]Legacy Config(.eslintrc)
{ "extends": ["plugin:@tanstack/query/recommended"] }或自定义:
{ "plugins": ["@tanstack/query"], "rules": { "@tanstack/query/stable-query-client": "error" } }完整的插件配置说明可参见 ESLint Plugin 文档,本规则在规则列表中的位置见该文档的 Rules 一节;其他规则文档如 exhaustive-deps、no-unstable-deps 可作为配套阅读。
小结
stable-query-client是 TanStack Query 官方 ESLint 插件中一条推荐开启(error级别)且可自动修复的规则,它针对的是一条朴素但高价值的约束:QueryClient 持有 QueryCache,一个应用只能有一个长期存活的实例。掌握它需要理解三层内容:
- 行为层:组件/自定义 Hook 内直接
new QueryClient()会被报告,推荐改写为const [queryClient] = useState(() => new QueryClient())或提升为模块级常量; - 边界层:规则仅对从
@tanstack/react-query导入的QueryClient生效,且豁免非组件函数(非use*/非大写命名)与 async Server Component; - 实现层:触发判定基于 NewExpression 与变量声明的直接父子关系,自动修复保留变量名与构造参数,解构声明形式只报错不修复。
理解了这些细节,你不仅能正确使用这条规则,也能在面对类似「某条 ESLint 规则为什么没报/误报」的问题时,像本文一样从规则源码、导入检测工具与 RuleTester 用例三个层面快速定位其行为边界。
【免费下载链接】query🤖 Powerful asynchronous state management, server-state utilities and data fetching for the web. TS/JS, React Query, Solid Query, Svelte Query and Vue Query.项目地址: https://gitcode.com/GitHub_Trending/qu/query
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考