Storybook for TanStack React 集成 TanStack Query在 .storybook/preview 中配置 QueryClient 的完整指南【免费下载链接】storybookStorybook is the industry standard workshop for building, documenting, and testing UI components in isolation项目地址: https://gitcode.com/GitHub_Trending/st/storybook导读本篇指南聚焦于 Storybook 的 TanStack React 框架即storybook/tanstack-react中如何手动接入 TanStack Query 的完整配置方案。你将掌握在.storybook/preview.tsx中创建并共享单一QueryClient、通过beforeEach清空缓存实现故事间隔离、利用parameters.tanstack.router.context让路由 loader 与测试共享同一客户端以及在单个故事中通过setQueryData预置查询数据等方法并配合框架源码理解其底层运行机制。为什么 TanStack Query 需要手动配置storybook/tanstack-react是 Storybook 官方为基于 React Vite 的 TanStack Router。它建立在storybook/react-vite之上并自动完成三件事每个故事被包裹在一个基于内存历史createMemoryHistory的 TanStack Router 实例中无需启动完整应用外壳即可获得可用的路由上下文自动将tanstack/react-router的导入重定向到 Storybook 兼容的 mock 层使useNavigate()、useSearch()、useParams()等 hook 在故事中可用导航行为可被监听自动拦截tanstack/react-start等模块把createServerFn().handler(...)替换为可观察、可覆写的 mock 函数。但 TanStack Query 不在自动初始化范围内——框架不会替你创建QueryClient也不会自动包裹QueryClientProvider。框架文档在“TanStack Query”一节中明确指出推荐做法是在 preview 文件中创建单一QueryClient通过beforeEach在故事之间清空它并让同一个实例同时出现在parameters.tanstack.router.context与QueryClientProvider装饰器中。这正是本指南要展开的核心配置。完整配置.storybook/preview.tsx下面这份配置来自仓库的官方代码片段 docs/_snippets/tanstack-react-query-setup.md它同时提供了两种写法传统 CSF 3 的Preview导出以及实验性的 CSF NextdefinePreviewAPI。两份代码功能完全等价。写法一CSF 3Preview导出import { type QueryClient, QueryClientProvider } from tanstack/react-query; import type { Preview } from storybook/tanstack-react; // Create a new QueryClient const queryClient new QueryClient({ defaultOptions: { queries: { retry: false, staleTime: Infinity, }, }, }); const preview: Preview { beforeEach: () { // Clear the cache between stories so each story starts fresh queryClient.clear(); }, parameters: { tanstack: { router: { // Make queryClient available in stories beforeEach via ctx.context.queryClient context: { queryClient }, }, }, }, decorators: [ (Story) ( // Provide the QueryClient to all stories QueryClientProvider client{queryClient} Story / /QueryClientProvider ), ], }; export default preview;写法二CSF NextdefinePreviewimport { definePreview } from storybook/tanstack-react; import { type QueryClient, QueryClientProvider } from tanstack/react-query; // Create a new QueryClient const queryClient new QueryClient({ defaultOptions: { queries: { retry: false, staleTime: Infinity, }, }, }); export default definePreview({ beforeEach: () { // Clear the cache between stories so each story starts fresh queryClient.clear(); }, parameters: { tanstack: { router: { // Make queryClient available in stories beforeEach via ctx.context.queryClient context: { queryClient }, }, }, }, decorators: [ (Story) ( // Provide the QueryClient to all stories QueryClientProvider client{queryClient} Story / /QueryClientProvider ), ], });逐段拆解这份配置到底做了什么配置片段作用说明new QueryClient({ defaultOptions: { queries: { retry: false, staleTime: Infinity } } })创建唯一客户端实例retry: false避免查询失败后自动重试拖慢交互测试与快照staleTime: Infinity让数据在故事生命周期内始终视为新鲜减少因时序导致的不确定渲染beforeEach: () queryClient.clear()故事间缓存隔离每个故事渲染前清空全部查询缓存保证上一故事setQueryData或真实请求留下的数据不会泄漏到下一个故事parameters.tanstack.router.context { queryClient }向路由上下文注入客户端使该实例可通过ctx.context.queryClient在故事的beforeEach、路由loader与beforeLoad中访问见下文源码分析decorators: [(Story) QueryClientProvider client{queryClient}Story //QueryClientProvider]向所有故事提供 React 上下文让组件内useQuery()/useQueryClient()都能拿到与路由上下文同一个客户端实例关键在于“同一个实例”被同时注入两个位置路由上下文供 loader/beforeLoad/测试访问与 React Provider供组件渲染访问。框架文档特别警告如果这两个位置拿到的是不同的客户端路由 loader 与组件可能读到不同的缓存故事里调用setQueryData也未必会影响正在渲染的组件。在单个故事中预置查询数据Seeding Query Data有了上述全局配置你就可以在具体故事中通过beforeEach直接向共享客户端写入数据从而无需真实网络请求即可演示加载成功、空态、登录态等场景。官方配套片段 docs/_snippets/tanstack-react-query-in-story.md 给出了完整示例import type { Meta, StoryObj } from storybook/tanstack-react; import type { QueryClient } from tanstack/react-query; import { Navbar } from ./Navbar; const meta { component: Navbar, } satisfies Metatypeof Navbar; export default meta; type Story StoryObjtypeof meta; export const Default: Story {}; export const LoggedIn: Story { beforeEach: async ({ parameters }) { const qc: QueryClient parameters.tanstack?.router?.context?.queryClient; qc?.setQueryData([currentUser], { id: user-1, name: Ada Lovelace, }); }, };对应 CSF Next 的写法meta.story()定义故事import type { QueryClient } from tanstack/react-query; import preview from ../.storybook/preview; import { Navbar } from ./Navbar; const meta preview.meta({ component: Navbar, }); export const Default meta.story(); export const LoggedIn meta.story({ beforeEach: async ({ parameters }) { const qc: QueryClient parameters.tanstack?.router?.context?.queryClient; qc?.setQueryData([currentUser], { id: user-1, name: Ada Lovelace, }); }, });执行时序全局beforeEach清空缓存先于故事级beforeEach写入数据运行随后组件才渲染。因此LoggedIn故事渲染时useQuery([currentUser])能直接命中内存缓存并展示登录态而Default故事因为缓存已被清空且未写入保持初始状态。这是“每个故事从干净状态开始、按需预置数据”的标准实践。源码视角beforeEach 与 RouterContext 是如何串联起来的为了让上述配置不只是一段“能跑但说不清原理”的代码我们结合框架源码看两个关键机制。1.routerBeforeEach故事路由的创建与上下文注入框架在 preview.tsx 中导出了beforeEach: [routerBeforeEach]其实现位于 before-each.ts它读取context.parameters.tanstack?.router其中routerParameters.context既可以是一个静态对象也可以是一个工厂函数({ storyContext }) Recordstring, unknown工厂函数在路由初始加载之前、React 渲染之外被调用因此其返回值对路由loader与beforeLoad可见——这正是你在 preview 里通过context: { queryClient }传入的实例能出现在ctx.context.queryClient中的原因每个故事的路由被缓存在模块级storyRoutersMap 中按context.id并在故事结束后清理从而保证故事间的路由与上下文互不污染。2.tanstackRouteDecorator用 RouterProvider 包裹故事同样在 preview.tsx 中applyDecorators会把tanstackRouteDecorator置于内层其实现位于 decorator.tsx装饰器从context.tanstackRouter取出 beforeEach 阶段创建好的路由器用RouterProvider router{router} context{providerContext}包裹故事组件它还支持parameters.tanstack.router.useRouterContext——一个在渲染期间执行的 React hook例如useRouterContext: () ({ queryClient: useQueryClient() })。官方文档明确提醒由于它是 hook运行于路由初始加载之后其值只能被渲染出的组件读取而无法被初始loader/beforeLoad读取若 loader 需要从路由上下文取值必须改用context工厂。从 types.ts 的类型定义可以看到RouterParameters完整字段route、path、params、query、routeOverrides、context、useRouterContext。本文涉及的context只是其中之一其余字段用于路由渲染与覆写例如routeOverrides可在不修改原始路由对象的前提下按路由 ID 覆写loader、beforeLoad、validateSearch、loaderDeps、context用__root__定位根路由。3. 参数类型的“可推导”特性从源码看RouterParameters针对文件路由做了类型约束params的类型ResolveParamsPath会收敛到该路由路径声明的参数名如/$id对应的{ id: string }query会收敛到该路由声明的 search 参数类型。也就是说你在 story 中书写parameters.tanstack.router.params / query时可以获得完整的类型提示与校验。一个客户端还是每个故事一个客户端官方文档同时给出了两种策略及取舍共享单一客户端推荐默认在 preview 中创建一次beforeEach清空缓存。优点是接入点唯一、心智负担小故事在侧边栏、Docs 页、portable stories 或测试运行中无论以何种方式渲染路由上下文与 React Provider 始终指向同一个实例setQueryData的时序也容易把控。每个故事独立客户端当需要更强隔离时例如同一 Docs 页面上多个故事使用相同 query key 却希望缓存响应互不干扰可以为每个故事单独创建客户端。代价是必须同时把它接入该故事的router.context与QueryClientProvider两处否则出现双缓存不一致问题并且需要显式清理每个客户端持有的定时器、订阅与缓存数据否则容易造成内存泄漏或跨故事串扰。对绝大多数场景先采用共享客户端 beforeEach清空的方案即可。常见问题与排查故事渲染报错“modules not providing a default export”通常是服务端专属模块如数据库客户端、auth 库被浏览器加载。这属于 TanStack Start 依赖处理问题与本配置无关——排查思路是沿错误堆栈找到自己写的、最靠近 Node 依赖的那个模块为其添加__mocks__文件而不是 mock 组件或路由本身详见 docs/get-started/frameworks/tanstack-react.mdx 的“Handling server-only dependencies”一节。setQueryData后组件没有反应优先确认你在 story 的beforeEach中读取的是parameters.tanstack?.router?.context?.queryClient且与 preview 装饰器中的QueryClientProvider是同一实例——这是文档明确指出的双客户端陷阱。需要访问 queryClient 但无法在 beforeEach 中使用若你是在组件渲染期间获取客户端例如想用useQueryClient()动态取值请改用parameters.tanstack.router.useRouterContext同时接受它无法被初始 loader 读取的限制。小结与验证将本指南的 preview 配置与 story 级setQueryData组合使用即可在 Storybook 中为依赖 TanStack Query 的组件提供稳定、隔离、可预置数据的运行环境。配置完成后运行npx storybook dev即可在浏览器中验证故事渲染若配合storybook/addon-vitest等测试工具运行交互测试retry: false与staleTime: Infinity也有助于获得确定性的测试结果。你可以继续在 docs/get-started/frameworks/tanstack-react.mdx 中查阅路由渲染、routeOverrides、服务端函数 mock 等更多框架能力或在 code/frameworks/tanstack-react/src 下阅读本文涉及的before-each.ts、decorator.tsx、types.ts等源码实现。【免费下载链接】storybookStorybook is the industry standard workshop for building, documenting, and testing UI components in isolation项目地址: https://gitcode.com/GitHub_Trending/st/storybook创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考