禁止在客户端上下文中使用 Async 组件:深入解析 TanStack Start 的 `no-async-client-component` ESLint 规则
2026/9/15 14:21:18 网站建设 项目流程

禁止在客户端上下文中使用 Async 组件:深入解析 TanStack Start 的no-async-client-componentESLint 规则

【免费下载链接】router🤖 A client-first, server-capable, fully type-safe router and full-stack framework for the web (React and more).项目地址: https://gitcode.com/GitHub_Trending/ro/router

在 TanStack Start 的 Server Components 架构中,Async 组件只能在服务端组件(Server Components)上下文中合法存在。一旦某个 Async 组件因为被路由选项引用、处于'use client'文件、或被客户端组件渲染而"变成"客户端组件,运行时就会直接崩溃——而且这类错误往往发生在页面渲染过程中,难以提前发现。TanStack Start 官方 ESLint 插件提供的@tanstack/start/no-async-client-component规则(文档见 no-async-client-component.md),正是为了在编译期、静态分析阶段就把这类隐患拦截下来。读完本文,你将掌握该规则的完整语义、两种报告位置的区别、如何用渲染图(render graph)机制追踪"客户端化"的组件,以及如何在实际项目中配置与修复这类问题。

规则背景:为什么 Async 组件不能出现在客户端上下文

Async React 组件(即用async关键字声明的组件函数)在标准客户端 React 中并不被支持——客户端 React 无法等待组件函数返回的 Promise 来继续渲染。它们只有在 React Server Components(RSC)的运行时中才是合法的:服务端渲染管线会解析组件的 Promise,把await到的数据渲染成最终的 UI。

在 TanStack Start 中,这意味着 Async 组件只能出现在服务端组件上下文里,具体有两种合法形态:

  • 通过renderServerComponent(<AsyncComp />)渲染(render-graph-builder.ts 中将其识别为服务端根节点);
  • createCompositeComponent((props) => <AsyncComp />)的回调内部渲染(即复合服务端组件的回调体内)。

而问题在于:一个组件可能"直接或间接"地变成客户端组件。例如,文件路由的component选项本质上是一个客户端渲染入口,被它引用的组件会在浏览器端被 React 挂载;一旦 Async 组件被这样的客户端入口引用,代码虽然能通过编译,但运行时会抛出崩溃错误。no-async-client-component规则的价值,就是把这种"迟早会爆炸"的代码在 lint 阶段揪出来。

规则详情:正确与错误的代码形态

不正确的代码

下面的路由页面组件被声明为async,然后被createFileRoutecomponent选项引用——这正是最常见的错误形态。该组件在浏览器端会被当作客户端组件渲染,运行时会崩溃:

/* eslint "@tanstack/start/no-async-client-component": "error" */ import { createFileRoute } from '@tanstack/react-router' export async function Page() { return <div /> } export const Route = createFileRoute(undefined)({ component: Page, })

正确的代码

方案一:去掉async。如果组件内部不需要await服务端数据,把它改回同步组件即可:

/* eslint "@tanstack/start/no-async-client-component": "error" */ import { createFileRoute } from '@tanstack/react-router' export function Page() { return <div /> } export const Route = createFileRoute(undefined)({ component: Page, })

方案二:让 Async 组件留在服务端上下文。如果确实需要await数据,就把它放进createCompositeComponent回调,使其保持在服务端组件边界之内:

/* eslint "@tanstack/start/no-async-client-component": "error" */ import { createCompositeComponent } from '@tanstack/react-start/rsc' export const ServerPage = createCompositeComponent(async () => { const message = await Promise.resolve('hello') return <div>{message}</div> })

值得注意的是,规则对 Async 组件的识别覆盖了两种常见声明形式:async function MyComponent() {}的函数声明,以及const MyComponent = async () => {}/const MyComponent = async function () {}的变量声明。源码中的 async-component-detector.ts 以 PascalCase 命名(首字母大写)+async修饰符两个条件来判定一个函数是不是"Async 组件"。

报告位置:Usage-site 与 Definition-site

该规则的报告点有两种,理解它们的区别有助于快速定位问题根源:

  • Usage-site(使用点报告):当组件是因为被路由选项引用而变成客户端组件时(如componentpendingComponenterrorComponentnotFoundComponent等),错误会报告在路由选项中引用该组件的位置(即路由文件里的 usage 处)。这样你能立刻看到"是哪个路由配置把 Async 组件拖进了客户端"。
  • Definition-site(定义点报告):当组件是因为文件带有'use client'指令被某个客户端组件渲染而成为客户端组件时,错误会报告在组件定义处

对应的两条报错消息分别以asyncClientComponentDefinitionasyncClientComponentUsage两个 messageId 定义在 no-async-client-component.rule.ts 中。判定逻辑在 context-analyzer.ts 中通过ClientReason区分三种原因:use-client(文件有'use client'指令)、route-option(被createFileRoute/createRootRoute等路由创建器的组件选项引用)、rendered-by-client(被客户端父组件渲染,报错信息中会带上父组件名与父文件路径)。

规则的推荐状态与配置方式

no-async-client-component被标记为Recommended(文档Attributes一节中标注 ✅ Recommended),意味着它被收录进插件的推荐配置,开箱即用。插件本身的安装与配置请参阅 eslint-plugin-start.md,这里给出与本规则直接相关的几种方式。

Flat Config(ESLint 9+,eslint.config.js

启用全部推荐规则(包含本规则):

// eslint.config.js import pluginStart from '@tanstack/eslint-plugin-start' export default [ pluginStart.configs['flat/recommended'], // Any other config... ]

只启用当前这条规则

// eslint.config.js import pluginStart from '@tanstack/eslint-plugin-start' export default [ { plugins: { '@tanstack/start': pluginStart, }, rules: { '@tanstack/start/no-async-client-component': 'error', }, }, // Any other config... ]

Legacy Config(.eslintrc

{ "extends": ["plugin:@tanstack/eslint-plugin-start/recommended"] }

或自定义:

{ "plugins": ["@tanstack/eslint-plugin-start"], "rules": { "@tanstack/start/no-async-client-component": "error" } }

插件内部在 index.ts 中同时导出了recommendedflat/recommended两套配置,两者都把no-client-code-in-server-component与本规则设为error级别;而 rules.ts 则把两条规则统一注册进插件。

可选项:ignorePatterns

规则还支持一个可选的ignorePatterns选项(数组,元素为文件路径子串),用于忽略某些文件的检查。凡是文件路径包含列表中任意子串的文件都会被跳过(见 no-async-client-component.rule.ts 中的shouldIgnore实现)。例如:

rules: { '@tanstack/start/no-async-client-component': ['error', { ignorePatterns: ['legacy/', 'generated/'], }], }

原理剖析:渲染图 + 上下文传播的静态分析

这条规则并不是简单的"看到 async 函数就报错",而是构建了一张跨文件的组件渲染图(render graph)来做可达性分析,其核心实现分布在规则目录下的 4 个文件中:

  1. render-graph-builder.ts:遍历 TypeScript Program 的所有源文件,索引组件(fileName:ComponentName为 key)、JSX 渲染边(<Comp />形成的 from→to 边)、服务端根节点(renderServerComponent/createCompositeComponent)、客户端根节点(路由组件选项引用、'use client'文件内的组件)以及路由选项的使用位置信息。
  2. context-analyzer.ts:在渲染图上做 server/client 上下文的传播。算法分三个阶段:先从服务端根节点向下传播服务端上下文(遇到'use client'边界即停止);再从客户端根节点向下传播客户端上下文;最后对'use client'文件中的组件强制标记为客户端。如果一个组件同时从两条路径可达,客户端上下文优先("tainted",被污染)——这正是"直接或间接变成客户端"的判定核心。
  3. async-component-detector.ts:负责识别文件中的 Async 组件(PascalCase 命名 +async声明/初始化)。
  4. no-async-client-component.rule.ts:规则主入口,负责按上述报告策略输出诊断。

为兼顾性能,主规则文件做了多处缓存与门控(gating)优化:

  • 每个ts.Program只构建一次完整渲染图(perProgramCache,按需构建graphBuilt标记);
  • 对每个入口文件按需切片出可达子图sliceGraphForEntryFile)再进行分析,并通过onDemandCache缓存分析结果;
  • Program()钩子中先做快速文本级门控(fileContainsRouteOptions/fileContainsServerComponentRoots/fileHasUseClientDirective),绝大多数不相关的文件直接跳过,避免无谓的全程序图构建;
  • adjacencyCache缓存邻接表,避免每次切片都做全边扫描。

一个值得注意的细节是:路由创建器的识别不仅支持createFileRoute('/x')({...})这种柯里化调用,还支持createRootRoute()createRootRouteWithContext<T>()以及链式调用(createFileRoute('/x').update(...)({...})),getRouteOptionsObject/findRouteCreatorFromCallee会沿着 callee 一路回溯寻找真正的路由创建器来源。

测试用例验证:规则行为一览

规则测试位于 no-async-client-component.rule.test.ts,覆盖了非常完整的行为矩阵,可以直接当作"规则行为规范"阅读:

合法(valid)场景:

  • 路由中的同步组件(component: SyncComponent);
  • createCompositeComponent(async () => { ... })内的 Async 组件;
  • 'use client'文件中的同步组件;
  • 路由中内联的同步箭头组件(component: () => <div>Hello</div>);
  • 同步pendingComponent
  • createRootRoute中的同步组件。

非法(invalid)场景(均报asyncClientComponentDefinition):

  • Async 组件直接作为component
  • 'use client'文件中的 Async 组件(export async function AsyncClientComponent());
  • Async 箭头组件作为componentconst AsyncPage = async () => {...});
  • AsyncpendingComponent
  • AsyncerrorComponent
  • createRootRoute中的 Async 组件;
  • createRootRouteWithContext<MyRouterContext>中的 Async 组件;
  • 同一文件内多个 Async 组件被不同路由选项引用(每个各报一次错误)。

从测试可以看出规则的两大边界:"Async 组件 + 客户端入口 = 报错",而**"同步组件无论在哪、Async 组件只要在服务端边界内 = 放行"**。

常见问题与排查建议

Q1:报错说组件"cannot be used in client context",但我的组件没有写'use client'答:很可能是被路由选项(component/pendingComponent/errorComponent/notFoundComponent)引用了,或是在某个客户端组件内部被<Comp />渲染。检查报错位置是 Usage-site(路由文件里)还是 Definition-site(组件定义处),前者会直接指向引用它的路由配置。

Q2:报错信息里的reason后缀是什么意思?答:规则消息会拼接一条原因说明:File has "use client" directive.Component is referenced by a route option (createFileRoute/createRootRoute).Rendered by client component "X" in <路径>.(由formatReason生成),帮助你一眼判断客户端化的根因。

Q3:我确实需要客户端渲染一个"看起来像异步"的组件,怎么办?答:客户端组件本质上无法await,请改用同步组件配合数据请求方案(如 TanStack Query、loader 数据预取等)在渲染前把数据准备好;只有服务端组件(renderServerComponent/createCompositeComponent)才允许 Async 组件,这是规则坚持的边界。

Q4:规则会误报构建产物或第三方文件吗?答:渲染图构建时会跳过.d.ts声明文件和node_modules(见 render-graph-builder.ts 的build()预过滤),若仍有特定目录需要豁免,可使用上文提到的ignorePatterns选项。

小结

@tanstack/start/no-async-client-component是 TanStack Start 客户端/服务端边界安全的重要防线:它把"Async 组件只能在服务端上下文使用"这条运行时约束提前到了 lint 阶段,并通过跨文件的渲染图分析精确识别"直接或间接客户端化"的组件。配合插件另一条推荐规则no-client-code-in-server-component(见 no-client-code-in-server-component.md),可以在编码阶段就系统性地守住 RSC 边界,避免把运行时崩溃留给用户。

【免费下载链接】router🤖 A client-first, server-capable, fully type-safe router and full-stack framework for the web (React and more).项目地址: https://gitcode.com/GitHub_Trending/ro/router

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询