Storybook 集成 TanStack React 框架:从 @storybook/react-vite 迁移到 @storybook/tanstack-react 的完整指南
2026/9/10 18:15:49 网站建设 项目流程

Storybook 集成 TanStack React 框架:从 @storybook/react-vite 迁移到 @storybook/tanstack-react 的完整指南

【免费下载链接】storybookStorybook is the industry standard workshop for building, documenting, and testing UI components in isolation项目地址: https://gitcode.com/GitHub_Trending/st/storybook

在 Storybook 官方框架体系中,@storybook/tanstack-react是专为基于 TanStack Router 与 TanStack Start 构建的 React + Vite 应用设计的框架集成。本文以在.storybook/main.ts中将框架从@storybook/react-vite切换为@storybook/tanstack-react为核心操作,系统讲解安装步骤、CSF 3 与 CSF Next 两种配置写法、自动化迁移工具,以及迁移后如何利用路由感知渲染、自动 Mock 与 TanStack Query 集成能力,帮助你在 Storybook 中无需启动完整应用即可独立开发、测试和文档化依赖路由与服务端函数(Server Functions)的组件。

一、为什么需要 @storybook/tanstack-react

@storybook/tanstack-react是 Storybook 针对 TanStack Router 和 TanStack Start 应用提供的框架集成,它建立在@storybook/react-vite(参见 docs/get-started/frameworks/react-vite.mdx)之上,额外带来三层核心能力(来源:docs/get-started/frameworks/tanstack-react.mdx):

  1. 路由感知渲染:自动用内存路由(Memory-backed Router)包裹每个 Story,提供可用的 Router 上下文,无需启动完整应用外壳;
  2. 自动 Router Mock:将@tanstack/react-router的导入重定向到 Storybook 兼容的 Mock 层,useNavigateuseSearchuseParams等 Hook 在 Story 中照常可用,导航行为可被观测;
  3. TanStack Start Mock:自动桩替换 Start 服务端与运行时入口点,使依赖 Server Functions 的组件可以直接在 Storybook 中渲染。

从仓库源码看,该框架位于 code/frameworks/tanstack-react,其 package.json 中明确声明依赖@storybook/builder-vite@storybook/react@storybook/react-vite,并在 peerDependencies 中要求@tanstack/react-router@tanstack/react-start(可选)、@tanstack/router-core@tanstack/start-client-core(可选)以及 React 与 Vite(参见 package.json)。

二、环境要求与安装

环境要求

官方文档(docs/get-started/frameworks/tanstack-react.mdx)给出的最低要求为:

  • React:≥ 18
  • Vite:≥ 7

同时要求项目中已存在 TanStack Router 应用(@tanstack/react-router可用);如果应用使用了 TanStack Start 的 API(如 Server Functions),还需要保留对应的 TanStack Start 包。从源码的 peerDependencies 看,该框架实际声明的范围更宽(Vite^5 || ^6 || ^7 || ^8,React^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0),但建议以官方文档的 React ≥ 18、Vite ≥ 7 作为稳妥基线。

安装框架包

在项目根目录执行安装(来源:docs/_snippets/tanstack-react-install.md):

npm install --save-dev @storybook/tanstack-react
pnpm add --save-dev @storybook/tanstack-react
yarn add --dev @storybook/tanstack-react

如果你是全新项目,也可以直接使用 Storybook 的创建命令初始化(见 docs/get-started/frameworks/tanstack-react.mdx 中的create命令),然后从编写 Story、运行测试、编写文档开始。

三、核心操作:在 .storybook/main.ts 中切换框架

@storybook/react-vite切换为@storybook/tanstack-react的核心操作是修改.storybook/main.ts中的framework属性。根据你的配置写法,有两种形式(来源:docs/_snippets/tanstack-react-add-framework.md)。

形式一:CSF 3(经典配置对象)

- import type { StorybookConfig } from '@storybook/react-vite'; + import type { StorybookConfig } from '@storybook/tanstack-react'; const config: StorybookConfig = { // ... - framework: '@storybook/react-vite', + framework: '@storybook/tanstack-react', }; export default config;

形式二:CSF Next(defineMain 工厂函数)

- import { defineMain } from '@storybook/react-vite/node'; + import { defineMain } from '@storybook/tanstack-react/node'; export default defineMain({ // ... - framework: '@storybook/react-vite', + framework: '@storybook/tanstack-react', });

注意两个细节:

  1. 类型导入同步切换:CSF 3 场景下,StorybookConfig类型要从@storybook/tanstack-react导入;CSF Next 场景下,defineMain要从@storybook/tanstack-react/node导入。这与框架包的导出结构一一对应——从 package.json 的exports字段可以看到,包根路径导出主入口,./node导出 Node 侧入口(供 CSF 工厂使用),此外还暴露了./preset./preview./react-router./start./start-storage-context等子路径。

  2. framework 属性可以携带 options:当你的.storybook/main.ts需要向 Vite 构建器传参时,可把framework写成对象形式(来源:docs/_snippets/tanstack-react-framework-options.md):

import type { StorybookConfig } from '@storybook/tanstack-react'; const config: StorybookConfig = { framework: { name: '@storybook/tanstack-react', options: { builder: { // Vite builder options }, }, }, }; export default config;
import { defineMain } from '@storybook/tanstack-react/node'; export default defineMain({ framework: { name: '@storybook/tanstack-react', options: { builder: { // Vite builder options }, }, }, });

其中options.builder的类型为Record<string, any>,用于配置框架底层的 Vite 构建器(详见 docs/builders/vite.mdx 与 docs/api/main-config/main-config-framework.mdx)。在源码层面,corepreset 会把该builder选项透传给@storybook/builder-vite(见 src/preset.ts)。

四、同步更新 preview 与类型引用

切换框架后,.storybook/preview.*中的类型导入也需要同步更新,保证类型检查一致(来源:docs/_snippets/tanstack-react-preview-migrate.md):

- import type { Preview } from '@storybook/react-vite'; + import type { Preview } from '@storybook/tanstack-react'; const preview: Preview = { //... }; export default preview;
- import { definePreview } from '@storybook/react-vite'; + import { definePreview } from '@storybook/tanstack-react'; export default definePreview({ //... });

同时,你的 Story 文件(*.stories.*)中MetaStoryObj类型也应从@storybook/tanstack-react导入,以启用parameters.tanstack.router的类型安全(下文会看到具体示例)。

五、自动化迁移:npx storybook automigrate

除了手工修改,Storybook 还提供了自动化迁移工具:

npx storybook automigrate react-vite-to-tanstack-react

根据官方文档(docs/get-started/frameworks/tanstack-react.mdx),该工具依次执行:

  1. 更新package.json,将@storybook/react-vite替换为@storybook/tanstack-react
  2. 更新.storybook/main.js|ts中的 framework 属性(同时兼容普通配置与 CSF 工厂defineMain配置);
  3. 扫描并更新所有引用@storybook/react-vite的 import 语句(包括 CSF 工厂使用的@storybook/react-vite/node),覆盖 Story 文件与 Storybook 配置文件;
  4. 检测.storybook/preview.*、其余.storybook/目录及所有*.stories.*中的手动 TanStack Router 装饰器,发现后会提供可复制的 AI 提示词,引导 AI 助手删除已冗余的装饰器。

迁移后的清理:删除手动 Router 装饰器

这一点需要特别强调:@storybook/tanstack-react已经自动为每个 Story 包裹 TanStack Router,因此迁移后任何手写的RouterProvider/createRouter/createMemoryHistory/createRootRoute装饰器都应删除。需要指定路由的 Story,请改用parameters.tanstack.router(见下一节)而不是手写装饰器。

六、迁移后能做什么:parameters.tanstack.router 全参数指南

切换框架后,你可以在 Story 的parameters.tanstack.router命名空间下声明路由行为。以下是该框架贡献的全部参数(来源:docs/get-started/frameworks/tanstack-react.mdx 的 API 章节)。

参数类型说明
routeAnyRoute \| route options object直接传入一个 Route 实例,或用一个包含path等选项的对象创建一个临时 Story 路由;Storybook 会自动从路由中提取 React 组件
pathstring设置 Story 路由的初始 URL 路径(也支持携带#fragment
queryRecord<string, unknown>向初始 URL 追加搜索参数(如?tab=details&page=2
paramsResolveParams<Path>将路由参数插值到当前路径;当route是类型化文件路由时,类型会被约束为该路由路径声明的参数名(例如/$id对应{ id: string }
routeOverridesPartial<Record<string, RouteOverrideOptions>>按路由 ID 覆盖路由选项,作用于 Story 路由与根路由;用'__root__'定位根路由,可覆盖loaderbeforeLoadvalidateSearchloaderDepscontext
contextRecord<string, unknown> \| (({ storyContext }) => Record<string, unknown>)注入 Story 路由的路由上下文;支持静态对象或接收 storyContext 的工厂函数,工厂在路由初始加载之前、React 渲染之外运行,因此其值可被loaderbeforeLoad读取
useRouterContext({ storyContext }) => RouterContext在渲染期间以 React Hook 方式计算路由上下文,适合读取只能从 React Provider 获取的值(如useQueryClient()

6.1 渲染一个 Route

将 TanStack Route 对象通过parameters.tanstack.router.route提供给 Story(来源:docs/_snippets/tanstack-react-route-story.md):

import type { Meta, StoryObj } from '@storybook/tanstack-react'; import { Route } from './Page'; const meta = { parameters: { layout: 'fullscreen', tanstack: { router: { route: Route, // 👈 在这里提供 Route // 👇 其余属性均为类型安全 params: { id: '42' }, query: { tab: 'details' }, }, }, }, } satisfies Meta<typeof Route>; export default meta; type Story = StoryObj<typeof meta>; export const Default: Story = {}; export const WithCustomLoader: Story = { parameters: { tanstack: { router: { route: Route, // 👈 在这里提供 Route params: { id: '42' }, routeOverrides: { '/items/$id': { loader: async () => ({ item: { id: '42', name: 'Loaded inside Storybook' }, }), }, }, }, }, }, };

6.2 处理动态参数(如 /$id)

params对象会被插值进 URL,routeOverrides则让你在不改动原始路由对象的前提下桩替换 loader(来源:docs/_snippets/tanstack-react-dynamic-params.md):

import type { Meta } from '@storybook/tanstack-react'; import { Route } from './$id'; const meta = { parameters: { tanstack: { router: { route: Route, params: { id: '42' }, routeOverrides: { '/showcase/$id': { loader: () => ({ item: mockItem }), }, }, }, }, }, } satisfies Meta<typeof Route>; export default meta;

6.3 渲染嵌套路由

route是连接到应用路由树(route tree)的文件路由时,Storybook 会自动向上回溯到根并复制整条路由树,父级布局路由(如_authenticated认证外壳)也会随之渲染。你也可以直接传入routeTree.gen.ts导出的routeTree(来源:docs/_snippets/tanstack-react-route-tree-story.md):

import type { Meta, StoryObj } from '@storybook/tanstack-react'; // 👇 路由文件是应用路由树的一部分 import { Route } from './routes/_authenticated/settings/profile'; const meta = { parameters: { tanstack: { router: { // 👇 Storybook 向上回溯到根并复制整条路由树, // 因此父级布局(如认证外壳)也会渲染 route: Route, path: '/settings/profile', // 👇 桩替换父级路由的守卫,让 Story 可以独立渲染 routeOverrides: { '/_authenticated': { beforeLoad: () => {} }, }, }, }, }, } satisfies Meta<typeof Route>; export default meta; type Story = StoryObj<typeof meta>; export const Default: Story = {};

6.4 对普通组件使用路由参数

如果 Story 渲染的是普通 React 组件而非路由对象,仍可通过parameters.tanstack.router提供路由上下文——组件可以正常读取useRouterStateuseSearchuseParamsuseLoaderData等 Hook(来源:docs/_snippets/tanstack-react-plain-component-story.md):

import type { Meta, StoryObj } from '@storybook/tanstack-react'; import { Page } from './Page'; const meta = { component: Page, } satisfies Meta<typeof Page>; export default meta; type Story = StoryObj<typeof meta>; export const Default: Story = { parameters: { tanstack: { router: { route: { path: '/demo/form/address', }, query: { view: 'list' }, }, }, }, };

6.5 定义搜索参数与 URL 片段(hash)

query定义搜索参数(如?tab=details&page=2),用path定义 URL 片段(如#section-name)(来源:docs/_snippets/tanstack-react-query-and-path.md):

export const WithHash: Story = { parameters: { tanstack: { // 👇 为路由提供 URL 片段(hash) router: { path: '/#section-name' }, }, }, }; export const WithSearch: Story = { parameters: { tanstack: { // 👇 为路由提供查询字符串 router: { query: { tab: 'details', page: '2' } }, }, }, };

6.6 按 Story 覆盖路由选项

当路由的loaderbeforeLoad会调用真实 API 时,可以在不修改原始路由对象的前提下,通过routeOverrides按 Story 覆盖。每个 key 是一个路由 ID,值可覆盖loaderbeforeLoadvalidateSearchloaderDepscontext;用'__root__'定位根路由(来源:docs/_snippets/tanstack-react-route-tree-overrides.md):

const meta = { title: 'Users/UserCard', parameters: { tanstack: { router: { route: Route, params: { userId: '42' }, // 👇 覆盖路由的 loader,让 Story 不调用真实 API routeOverrides: { '/users/$userId': { loader: async () => ({ user: { id: '42', name: 'Ada Lovelace' } }), }, }, }, }, }, } satisfies Meta<typeof Route>;

七、底层原理:自动 Mock 是如何实现的

从源码看,该框架的自动 Mock 能力由 src/preset.ts 中的viteFinal统一装配。它在@storybook/react-vite的 Vite 配置基础上追加了三个自定义插件:

  • moduleInterceptionPlugin(src/plugins/module-interception.ts):在模块解析阶段,把@tanstack/react-router(及其子路径)导入重定向到@storybook/tanstack-react/react-routerMock 模块;同时拦截@tanstack/react-start@tanstack/react-start/server@tanstack/react-start-server@tanstack/start-server-core等 Start 服务端模块以及virtual:cloudflareserver-entryworker-entry等虚拟模块,防止它们进入浏览器;
  • serverCodeEliminationPlugin(src/plugins/server-code-elimination.ts):做服务端代码消除;
  • serverOnlyStubPlugin(src/plugins/server-only-stub.ts):桩替换服务端专用模块。

此外,preset 还通过previewAnnotations注入了@storybook/tanstack-react/preview(src/preview.tsx),并通过optimizeViteDeps预优化了@tanstack/react-router的依赖链。

对外,框架包暴露了./react-router./start两个子路径(见 package.json 与 src/export-mocks),分别提供 TanStack Router 与 TanStack Start 兼容的 Mock 实现(包括 Mock 化的createServerFn())。测试中如需断言导航 Spy,可显式从这些模块导入。

八、在 Story 中 Mock Server Functions

如果组件导入了 TanStack Start 的 Server Function,Storybook 会把createServerFn().handler(...)的结果变成 Mock 函数,你可以用标准 Mock API 按 Story 覆盖,从而无需改动应用代码即可展示加载、成功、失败等状态(来源:docs/_snippets/tanstack-react-mock-server-fn-stories.md):

import type { Meta, StoryObj } from '@storybook/tanstack-react'; import { expect, mocked } from 'storybook/test'; import { updateProfile } from '../lib/updateProfile'; import { ProfileForm } from './ProfileForm'; const meta = { component: ProfileForm, } satisfies Meta<typeof ProfileForm>; export default meta; type Story = StoryObj<typeof meta>; export const Success: Story = { beforeEach: async () => { mocked(updateProfile).mockResolvedValue({ ok: true, name: 'Ada Lovelace' }); }, play: async ({ canvas, userEvent }) => { await userEvent.type(canvas.getByLabelText('Name'), 'Ada Lovelace'); await userEvent.click(canvas.getByRole('button', { name: 'Save profile' })); await expect(updateProfile).toHaveBeenCalled(); }, }; export const Failure: Story = { beforeEach: async () => { mocked(updateProfile).mockRejectedValue(new Error('Could not save profile')); }, };

九、处理 Server-only 依赖(三层策略)

TanStack Start 应用常在路由文件的模块作用域内导入服务端专用包(如数据库客户端、认证库),这些包在浏览器中会崩溃。该框架分三层处理:

第一层:框架级 Mock(自动)。preset 已拦截@tanstack/react-start@tanstack/react-start/server@tanstack/start-storage-context等模块,并把createServerFn()处理器替换为 Mock 函数,无需你做任何事。

第二层:应用级服务端模块。当路由导入了应用自己的服务端代码(如~/db/client~/auth/index.server)时,需要用__mocks__文件阻止真实模块及其 Node.js 依赖加载进浏览器。

第 1 步,在.storybook/preview.ts注册 Mock(来源:docs/_snippets/tanstack-react-mock-module-preview.md):

import { sb } from 'storybook/test'; // 阻止 postgres(仅限 Node)加载进浏览器 sb.mock(import('../src/db/client.ts')); export default {};

第 2 步,在真实模块旁创建src/db/__mocks__/client.ts,只使用import type,避免引入任何服务端包。

这里要解释清楚为什么用__mocks__文件而不是自动 Mock:Storybook 的自动 Mock 只替换函数,但仍会求值原始模块及其导入。对于导入了postgrespg等 Node.js 专属包的模块,原始模块绝不能被求值,否则浏览器会崩溃;__mocks__文件是唯一能完全阻止原始模块及其依赖链求值的方案(详见 docs/writing-stories/mocking-data-and-modules/mocking-modules.mdx)。

第三层:识别需要 Mock 的模块。报错如does not provide an export named 'default'AsyncLocalStorage is not defined,说明服务端专用模块进入了浏览器。修复方法是 Mock服务端模块本身,而不是使用它的组件或路由。例如Dashboard.tsx导入~/auth/session~/auth/session导入~/db/client~/db/client导入postgres——就 Mock~/db/client。Node.js 依赖(postgres)是"冒烟枪",Mock 离它最近、且由你掌控的模块即可。

两种不需要 Mock 的情况:

  • 模块来自@tanstack/*——框架 preset 已处理,请确保@storybook/tanstack-react是最新版本;
  • 模块只导入了createServerFn——已被 Mock,报错来自同一文件中的其他导入。

十、与 TanStack Query 协同使用

该框架可与 TanStack Query 配合,在 Storybook 中提供可用的 QueryClient 并按 Story 预置查询数据。TanStack Query 不会被自动配置,官方推荐方案是:在 preview 文件中创建单一QueryClient,通过beforeEach在 Story 之间清空缓存,并让同一个实例同时进入parameters.tanstack.router.contextQueryClientProvider装饰器(来源:docs/_snippets/tanstack-react-query-setup.md):

import { type QueryClient, QueryClientProvider } from '@tanstack/react-query'; import type { Preview } from '@storybook/tanstack-react'; // 👇 创建新的 QueryClient const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false, staleTime: Infinity, }, }, }); const preview: Preview = { beforeEach: () => { // 👇 在 Story 之间清空缓存,让每个 Story 从全新状态开始 queryClient.clear(); }, parameters: { tanstack: { router: { // 👇 让 queryClient 通过 ctx.context.queryClient 在 Story 的 beforeEach 中可用 context: { queryClient }, }, }, }, decorators: [ (Story) => ( // 👇 向所有 Story 提供 QueryClient <QueryClientProvider client={queryClient}> <Story /> </QueryClientProvider> ), ], }; export default preview;

这样无论 Storybook 以何种方式渲染 Story(侧边栏、Docs 页、portable stories 或测试运行),路由上下文与 React Provider 都指向同一个 QueryClient;每个 Story 在渲染前清空缓存,获得全新的查询状态。

在单个 Story 中,用beforeEach对共享 QueryClient 调用setQueryData预置数据,通过parameters.tanstack.router.context取出实例(来源:docs/_snippets/tanstack-react-query-in-story.md):

export const LoggedIn: Story = { beforeEach: async ({ parameters }) => { const qc: QueryClient = parameters.tanstack?.router?.context?.queryClient; qc?.setQueryData(['currentUser'], { id: 'user-1', name: 'Ada Lovelace', }); }, };

如果需要更强的隔离(例如在同一个 Docs 页面上渲染多个使用相同 query key 的 Story 并保持各自缓存独立),也可以为每个 Story 创建独立的 QueryClient,但必须让路由上下文与QueryClientProvider指向同一个实例,并显式清理每个客户端持有的定时器、订阅与缓存数据。

十一、FAQ 与常见问题

何时用@storybook/tanstack-react而非@storybook/react-vite当组件依赖 TanStack Router 或 TanStack Start 的 API,且需要 Storybook 提供路由上下文、类型化路由参数、自动 Router Mock 与 Mock 化的 Start Server Function 行为时使用前者(参见 docs/get-started/frameworks/react-vite.mdx);标准 React + Vite 且不使用 TanStack Router 的项目继续使用@storybook/react-vite

样式在 Storybook 中丢失怎么办?.storybook/preview.*中导入应用 CSS,使其随 preview 一起打包:

import '../src/styles/app.css';

详见 docs/configure/styling-and-css.mdx。

如何为所有 Story 提供 React Context Provider(主题、toast、认证等)?使用项目级装饰器(docs/writing-stories/decorators.mdx 中的全局装饰器)为所有 Story 提供 Provider;也可以在组件级或 Story 级装饰器中按需提供。

支持 React Server Components 吗?不支持。该框架使用内存路由在浏览器中运行 Story,而 React Server Components 需要服务端运行时。如果组件是 Server Component,请将客户端部分提取为 Client Component 再编写 Story。

报错"模块未提供 default 导出"?通常是服务端专用模块被导入进了浏览器。按第九节的流程沿错误堆栈定位模块并添加 Storybook Mock。

十二、小结

@storybook/react-vite切换到@storybook/tanstack-react本质上是三步:安装框架包、修改.storybook/main.tsframework属性(CSF 3 与 CSF Next 两种写法)、同步preview.*与 Story 文件的类型导入。之后你便获得了内存路由包裹、路由感知渲染、parameters.tanstack.router全参数控制、TanStack Router/Start 自动 Mock、Server-only 依赖三层治理以及与 TanStack Query 的无缝协同——这些能力在源码层由 preset.ts 中的 Vite 插件链(模块拦截、服务端代码消除、服务端专用模块桩替换)支撑实现。仓库还提供了可直接参考的框架内置示例 Story(见 template/stories,包含OutletPathlessLayoutRouterContextInjectionLoaderContextInjection等场景),可以作为上手与对照的最佳实践模板。

【免费下载链接】storybookStorybook is the industry standard workshop for building, documenting, and testing UI components in isolation项目地址: https://gitcode.com/GitHub_Trending/st/storybook

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

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

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

立即咨询