You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Storybook中使用tRPC?组件渲染报错求助

问题: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重复编写:

  1. 创建公共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>
  )
}
  1. 在.storybook/preview.tsx中配置全局装饰器:
import { AppProviders } from '../src/components/AppProviders'

export const decorators = [
  (Story) => (
    <AppProviders>
      <Story />
    </AppProviders>
  ),
]

这样所有Story都会自动拥有tRPC上下文,无需单独配置。

内容的提问来源于stack exchange,提问作者Erik Metz

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.31 23:05:27