如何在Storybook中使用tRPC?组件渲染报错求助
Cannot destructure property 'client' of 'useContext(...)' as it is null 报错详情
Cannot destructure property 'client' of 'useContext(...)' as it is null.
TypeError: Cannot destructure property 'client' of 'useContext(...)' as it is null.
at Object.useMutation$1 [as useMutation] (http://localhost:6006/vendors-node_modules_babel_runtime_helpers_esm_defineProperty_js-node_modules_trpc_next_dist_-afeaa2.iframe.bundle.js:6209:17)
at http://localhost:6006/vendors-node_modules_babel_runtime_helpers_esm_defineProperty_js-node_modules_trpc_next_dist_-afeaa2.iframe.bundle.js:5884:34
at Object.apply (http://localhost:6006/vendors-node_modules_babel_runtime_helpers_esm_defineProperty_js-node_modules_trpc_next_dist_-afeaa2.iframe.bundle.js:6583:20)
at PostSettings (http://localhost:6006/stories-components-feed-postPreview-HeaderPostPreview-stories.iframe.bundle.js:401:92)
at renderWithHooks (http://localhost:6006/vendors-node_modules_babel_runtime_helpers_esm_asyncToGenerator_js-node_modules_babel_runtime-86a76a.iframe.bundle.js:83400:18)
at mountIndeterminateComponent (http://localhost:6006/vendors-node_modules_babel_runtime_helpers_esm_asyncToGenerator_js-node_modules_babel_runtime-86a76a.iframe.bundle.js:87164:13)
at beginWork (http://localhost:6006/vendors-node_modules_babel_runtime_helpers_esm_asyncToGenerator_js-node_modules_babel_runtime-86a76a.iframe.bundle.js:88677:16)
at beginWork$1 (http://localhost:6006/vendors-node_modules_babel_runtime_helpers_esm_asyncToGenerator_js-node_modules_babel_runtime-86a76a.iframe.bundle.js:94516:14)
at performUnitOfWork (http://localhost:6006/vendors-node_modules_babel_runtime_helpers_esm_asyncToGenerator_js-node_modules_babel_runtime-86a76a.iframe.bundle.js:93647:12)
at workLoopSync (http://localhost:6006/vendors-node_modules_babel_runtime_helpers_esm_asyncToGenerator_js-node_modules_babel_runtime-86a76a.iframe.bundle.js:93556:5)
项目相关代码
@utils/trpc.ts
import { httpBatchLink, loggerLink } from '@trpc/client' import { createTRPCNext } from '@trpc/next' import { type inferRouterInputs, type inferRouterOutputs } from '@trpc/server' import superjson from 'superjson' import { IN_DEV } from '@constants/app' import { type AppRouter } from '@server/trpc/router/_app' const getBaseUrl = () => { if (typeof window !== 'undefined') return '' // browser should use relative url if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}` // SSR should use vercel url return `http://localhost:${process.env.PORT ?? 3000}` // dev SSR should use localhost } export const trpc = createTRPCNext<AppRouter>({ config() { return { transformer: superjson, links: [ loggerLink({ enabled: (opts) => IN_DEV || (opts.direction === 'down' && opts.result instanceof Error), }), httpBatchLink({ url: `${getBaseUrl()}/api/trpc`, }), ], } }, ssr: false, }) /** * Inference helper for inputs * @example type HelloInput = RouterInputs['example']['hello'] **/ export type RouterInputs = inferRouterInputs<AppRouter> /** * Inference helper for outputs * @example type HelloOutput = RouterOutputs['example']['hello'] **/ export type RouterOutputs = inferRouterOutputs<AppRouter>
PostPreview.stories.tsx
import React from 'react' import type { StoryFn, Meta } from '@storybook/react' import { trpc } from '@utils/trpc' const PostPreview = () => { const { data, status } = trpc.post.getAll.useQuery() if (status === 'loading') { return <p>loading</p> } if (status === 'error' || !data) { return <p>error</p> } return <div>{JSON.stringify(data)}</div> } export default { title: 'Library/PostPreview', component: PostPreview, argTypes: {}, } as Meta<typeof PostPreview> const Template: StoryFn<typeof PostPreview> = () => ( <PostPreview /> ) export const Standard = Template.bind({})
解决方案
问题根源
tRPC的React Hooks(如useQuery)依赖于TRPCProvider提供的上下文环境。在Storybook中,组件没有被该Provider包裹,导致useContext返回null,进而无法解构client属性。
解决方法
方法1:为单个Story添加装饰器
修改PostPreview.stories.tsx,创建用于Storybook的tRPC客户端,并通过TRPCProvider包裹组件:
import React from 'react' import type { StoryFn, Meta } from '@storybook/react' import { TRPCProvider, createTRPCReact } from '@trpc/react-query' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { httpBatchLink, loggerLink } from '@trpc/client' import superjson from 'superjson' import { type AppRouter } from '@server/trpc/router/_app' // 创建TRPC React实例 const trpc = createTRPCReact<AppRouter>() // 初始化React Query客户端 const queryClient = new QueryClient() // 创建Storybook专用tRPC客户端 const client = trpc.createClient({ transformer: superjson, links: [ loggerLink({ enabled: () => true, }), // 指向本地开发服务器的trpc接口,或者替换为mock链接 httpBatchLink({ url: 'http://localhost:3000/api/trpc', }), ], }) const PostPreview = () => { const { data, status } = trpc.post.getAll.useQuery() if (status === 'loading') { return <p>loading</p> } if (status === 'error' || !data) { return <p>error</p> } return <div>{JSON.stringify(data)}</div> } export default { title: 'Library/PostPreview', component: PostPreview, argTypes: {}, // 添加装饰器,注入TRPC上下文 decorators: [ (Story) => ( <QueryClientProvider client={queryClient}> <TRPCProvider client={client} queryClient={queryClient}> <Story /> </TRPCProvider> </QueryClientProvider> ), ], } as Meta<typeof PostPreview> const Template: StoryFn<typeof PostPreview> = () => <PostPreview /> export const Standard = Template.bind({})
方法2:使用Mock数据(无需连接真实服务器)
如果不想依赖后端服务,可以用tRPC的mockLink返回模拟数据:
// 替换上述client中的httpBatchLink为mockLink import { mockLink } from '@trpc/client' const client = trpc.createClient({ transformer: superjson, links: [ mockLink({ // 模拟post.getAll接口的返回值 post: { getAll: () => ({ data: [ { id: 1, title: 'Mock Post 1', content: 'This is a test post' }, { id: 2, title: 'Mock Post 2', content: 'Another test post' }, ], }), }, }), ], })
方法3:全局配置Provider(所有Story通用)
将TRPC和React Query的Provider抽成公共组件,在Storybook全局配置中挂载,避免每个Story重复编写:
- 创建公共Provider组件
src/components/AppProviders.tsx:
import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { type AppRouter } from '@server/trpc/router/_app' import { TRPCProvider, createTRPCReact } from '@trpc/react-query' import { httpBatchLink, loggerLink } from '@trpc/client' import superjson from 'superjson' import React from 'react' const trpc = createTRPCReact<AppRouter>() const queryClient = new QueryClient() const client = trpc.createClient({ transformer: superjson, links: [ loggerLink({ enabled: () => true, }), httpBatchLink({ url: 'http://localhost:3000/api/trpc', }), ], }) export const AppProviders = ({ children }: { children: React.ReactNode }) => { return ( <QueryClientProvider client={queryClient}> <TRPCProvider client={client} queryClient={queryClient}> {children} </TRPCProvider> </QueryClientProvider> ) }
- 在
.storybook/preview.tsx中配置全局装饰器:
import { AppProviders } from '../src/components/AppProviders' export const decorators = [ (Story) => ( <AppProviders> <Story /> </AppProviders> ), ]
这样所有Story都会自动拥有tRPC上下文,无需单独配置。
内容的提问来源于stack exchange,提问作者Erik Metz

