Storybook for TanStack React 实战:用 `path` 与 `query` 精确控制 Story 的 URL 哈希与查询参数
2026/9/10 16:14:11 网站建设 项目流程

Storybook for TanStack React 实战:用pathquery精确控制 Story 的 URL 哈希与查询参数

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

Storybook for TanStack React 是 Storybook 官方为 TanStack Router / TanStack Start 应用提供的框架集成,它在内存中为每个 story 启动一个 router,让依赖路由状态的组件无需启动完整应用即可独立渲染。本文聚焦该框架的parameters.tanstack.routerpathquery两个参数,讲解如何为单个 story 精确设置 URL 片段(hash,如#section-name)与查询字符串(search params,如?tab=details&page=2),并深入源码说明其底层实现。读完本文,你将能够在自己的 TanStack React story 中自由构造任意初始 URL 状态,用于锚点滚动、列表筛选、分页等场景的组件开发与测试。

背景:story 内的内存路由

根据框架文档 docs/get-started/frameworks/tanstack-react.mdx,@storybook/tanstack-react基于@storybook/react-vite构建,会自动把每个 story 包裹进一个内存路由(memory-backed TanStack Router)中。这意味着:

  • 不需要启动完整的应用外壳,story 内就有一个可用的 router context;
  • 可以为每个 story 单独设置初始路径、路由参数和查询字符串;
  • @tanstack/react-router的导入会被自动重定向到 Storybook 兼容的 mock 层,useNavigate()useSearch()useParams()等 hooks 在 story 中照常可用,导航动作会被记录为 Storybook spy。

pathquery正是在这个内存路由上工作的两个参数:它们共同决定 story 的"初始 URL"。框架文档在 Defining search params and URL fragments 一节中明确说明:用query设置 search params(如?tab=details&page=2),用path设置 URL 片段(如#section-name)。

核心用法:为 story 设置 hash 与 search params

关联文档 docs/_snippets/tanstack-react-query-and-path.md 提供了完整的可运行示例。下面按 Storybook 的两种写作范式分别展示。

CSF 3 写法

Page.stories.ts为例:meta 中通过parameters.tanstack.router.route指定要渲染的 TanStack Route 对象,story 级再通过path/query覆盖初始 URL 状态:

import type { Meta, StoryObj } from '@storybook/tanstack-react'; import { Route } from './Page'; const meta = { parameters: { tanstack: { router: { route: Route, }, }, }, } satisfies Meta<typeof Route>; export default meta; type Story = StoryObj<typeof meta>; export const WithHash: Story = { parameters: { tanstack: { // 👇 Provide the URL fragment (hash) for the route router: { path: '/#section-name' }, }, }, }; export const WithSearch: Story = { parameters: { tanstack: { // 👇 Provide the query string for the route router: { query: { tab: 'details', page: '2' } }, }, }, };

要点:

  • route: Route放在 meta 级,表示该组件组的所有 story 都渲染这个路由的组件;
  • WithHash通过path: '/#section-name'让内存路由的初始 URL 携带 hash 片段,适合验证"进入页面后自动滚动/定位到某个锚点"这类逻辑;
  • WithSearch通过query: { tab: 'details', page: '2' }让初始 URL 携带查询参数,适合验证列表页读取useSearch()并按条件渲染的场景;
  • 两者是各自 story 独立的,互不影响。

CSF Next 写法(实验性)

CSF Next 工厂函数式写法中,使用preview.meta()meta.story()组织同样的参数,语义与 CSF 3 完全一致:

import preview from '../.storybook/preview'; import { Route } from './Page'; const meta = preview.meta({ parameters: { tanstack: { router: { route: Route, }, }, }, }); export const WithHash = meta.story({ parameters: { tanstack: { // 👇 Provide the URL fragment (hash) for the route router: { path: '/#section-name' }, }, }, }); export const WithSearch = meta.story({ parameters: { tanstack: { // 👇 Provide the query string for the route router: { query: { tab: 'details', page: '2' } }, }, }, });

参数语义与类型约束

这两个参数在框架的类型定义 code/frameworks/tanstack-react/src/routing/types.ts 的RouterParameters接口中有明确声明:

  • path?: Path:设置 story 路由的初始 URL 路径。在 route tree 模式下,类型会被约束为应用中已注册的路径联合(如'/' | '/admin/users' | '/$libraryId/$version');在 app route 模式下可以是任意字符串(因为用户可能传入不在注册树中的自定义route)。
  • query?: Partial<StoryRouteSearch<TRoute>>:向初始 URL 追加 search params。当传入的route是文件路由(File Route)时,类型会进一步约束为该路由声明的allSearch类型,从而获得 search 字段的编译期检查;非文件路由场景则回退为Record<string, unknown>
  • params?: ResolveParams<Path>:用于把/$id这类动态段插值进路径,与path配合完成"带参数的路由地址"构造(详见 框架文档 Routing 一节)。

从类型设计可以看出,框架希望你在指定route的前提下使用querypath,这样既能享受类型安全,又能让内存路由的状态与真实路由定义保持一致。

源码级原理:内存路由如何消费 path 与 query

pathquery的消费发生在渲染 decorator 中,见 code/frameworks/tanstack-react/src/routing/decorator.tsx 的初始路径解析逻辑(L85-L114):

const routerParameters: RouterParameters = context.parameters.tanstack?.router ?? {}; // ... const inferredPath = routerParameters?.path || leaf.fullPath || (leaf.id ? normalizeFileRoutePath(leaf.id) : undefined) || mountPathFor(leaf); // Interpolate params into the path and append query/search params. let resolvedPath = interpolatePath({ path: inferredPath, params: routerParameters?.params ?? {}, }).interpolatedPath; const search = routerParameters?.query ? defaultStringifySearch(routerParameters.query) : ''; if (search) { resolvedPath += search; } const history = createMemoryHistory({ initialEntries: [resolvedPath], }); history.replace(resolvedPath); return createRouter({ // ... });

对应源码,可以总结出完整的路径解析优先级与拼接规则:

  1. 路径优先级routerParameters.path显式指定时优先使用;未指定时依次回退到叶子路由的fullPath、由路由 ID 归一化得到的文件路由路径(normalizeFileRoutePath,实现在 code/frameworks/tanstack-react/src/routing/path-utils.ts,负责剥离(group)路由组、_layout布局段与首尾下划线等文件路由命名噪音)、以及沿父链推导的挂载路径(mountPathFor)。
  2. 参数插值interpolatePath会把params对象(如{ id: '42' })插值进/$id这样的动态段,得到最终路径。
  3. search 拼接query对象经defaultStringifySearch序列化为 URL search string(如?tab=details&page=2)并直接拼接在路径之后。由于path中可以携带#hash,而 search 拼接在路径末尾,最终形成path + ?query的完整 URL。
  4. 内存历史:拼接结果作为initialEntries传给createMemoryHistory,并立即history.replace一次,随后createRouter使用这份内存历史创建 story 专属 router。这就是为什么 story 中useSearch()useParams()useRouterState()能读到与真实 URL 一致的状态。

另外,当 story 通过route参数传入 Route 对象时,code/frameworks/tanstack-react/src/routing/loader.ts 的routeComponentLoader会从路由选项中提取其 React 组件作为 story 渲染组件,并保留该 Route 以提供类型化的 router 配置——这也是query能获得该路由allSearch类型约束的前提。

组合实践:path + query + params 的完整初始状态

框架文档 Defining search params and URL fragments 说明querypath只是parameters.tanstack.router提供的多个属性之一,它们可以和其他属性组合出任意初始 URL。参照 docs/_snippets/tanstack-react-route-story.md 的示例,一个同时设置动态参数、查询参数、并 stub loader 的 story 形如:

import type { Meta, StoryObj } from '@storybook/tanstack-react'; import { Route } from './Page'; const meta = { parameters: { layout: 'fullscreen', tanstack: { router: { route: Route, // 👈 Supply the Route here // 👇 Rest of these properties are type-safe 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, // 👈 Supply the Route here // 👇 Rest of these properties are type-safe params: { id: '42' }, routeOverrides: { '/items/$id': { loader: async () => ({ item: { id: '42', name: 'Loaded inside Storybook' }, }), }, }, }, }, }, };

在此基础上叠加本文的pathquery即可得到完整形态:params负责动态段、query负责 search params、path负责路径与 hash,routeOverrides负责在不改动原路由对象的前提下替换loader/beforeLoad/validateSearch/loaderDeps/context。它们共同构成一个"story 级路由状态工厂",让同一个组件在不同 story 中呈现完全不同的 URL 语境。

典型场景与注意事项

  • 锚点定位验证path: '/#section-name'可用于测试"进入页面后根据 hash 滚动到指定区块"的行为,无需真实浏览器导航。
  • 筛选与分页态query: { tab: 'details', page: '2' }可让列表页 story 直接进入"已选中 details 标签、第二页"的状态,配合useSearch()渲染分支进行快照与交互测试。
  • search 校验:当route是带validateSearch的文件路由时,query的键会被类型约束到该路由声明的 search schema;如果路由对 search 有校验逻辑,确保 story 中提供的query能通过校验,否则内存路由初始加载可能失败。
  • 与 route tree 模式配合:当route是接入应用 route tree 的文件路由时,Storybook 会自动带上父级布局路由,此时用path导航到具体路由、用routeOverridesstub 祖先路由的 guards / loaders,即可让嵌套路由独立渲染(见 框架文档 Rendering nested routes)。
  • 类型回退:如果传入的route不在注册树中,pathquery的类型会放宽为任意字符串 /Record<string, unknown>,此时需要自行保证参数与路由语义一致。

小结

pathquery@storybook/tanstack-react框架parameters.tanstack.router命名空间下控制 story 初始 URL 的两个核心参数:path设置路径与 hash 片段,query追加 search params。它们在 decorator 中被解析、插值、序列化后作为createMemoryHistory的初始条目,驱动每个 story 独立的内存路由实例;配合routeparamsrouteOverrides,开发者可以为任意路由依赖组件构造精确可控的 URL 语境,从而在不启动完整应用的情况下完成组件开发、文档化与测试。相关示例代码可继续查阅 docs/_snippets/tanstack-react-query-and-path.md 与框架主文档 docs/get-started/frameworks/tanstack-react.mdx,底层实现可参考 code/frameworks/tanstack-react/src/routing/ 目录下的源码。

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

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

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

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

立即咨询