如何为React Hook useHass编写Storybook故事并生成参数返回值文档?
为React Hook编写Storybook故事的最优方案
问题背景
我负责的项目中有一个依赖上下文的React Hook,代码如下:
export function useHass(): HassContextProps { const context = useContext(HassContext); if (context === undefined) { throw new Error("useHass must be used within a HassProvider"); } return context; }
其返回值的TypeScript定义:
export interface HassContextProps { connection: Connection | null; setConnection: (connection: Connection) => void; getEntity: (entity: string) => HassEntity; getAllEntities: () => HassEntities; callService: ({ domain, service, serviceData, target, }: CallServiceArgs) => void; getStates: () => Promise<HassEntity[] | null>; getServices: () => Promise<HassServices | null>; getConfig: () => Promise<HassConfig | null>; getUser: () => Promise<HassUser | null>; ready: boolean; lastUpdated: Date; }
目前遇到的问题是:Storybook的ArgsTable对组件文档生成效果很好,但对Hook支持不佳,想知道为这个Hook编写Storybook故事、展示其参数及返回值的最优方式是什么,是否需要手动完成?
解决方案
核心思路
由于这个Hook依赖React上下文,首先要在Story中提供模拟的上下文Provider,避免Hook抛出错误;其次,因为Storybook对Hook的自动文档生成支持有限,需要手动补充返回值的文档说明,同时编写交互组件展示Hook的实际使用效果。
具体实现步骤
模拟上下文环境
用Storybook的decorators为所有Story包裹模拟的HassContext.Provider,提供预先定义好的上下文数据,确保Hook能正常运行。编写展示组件
在Story的render函数中创建一个测试组件,调用useHass并将返回值的属性、方法可视化展示,甚至添加交互按钮来模拟调用Hook的方法,让用户直观看到Hook的功能。手动补充文档
在Story的parameters.docs.argsTable.returns中手动定义返回值的每个字段说明,弥补自动生成的不足。
完整Story示例代码
import { Meta, StoryObj } from '@storybook/react'; import { useHass, HassContextProps, HassContext } from './useHass'; import { useMemo } from 'react'; const meta: Meta<typeof useHass> = { title: 'Hooks/useHass', component: useHass, parameters: { docs: { description: { component: '用于获取Home Assistant上下文的Hook,必须在HassProvider内部使用', }, }, }, }; export default meta; type Story = StoryObj<typeof useHass>; // 模拟HassContext的实现 const mockHassContext: HassContextProps = { connection: null, setConnection: (conn) => console.log('Set connection:', conn), getEntity: (entityId) => ({ entity_id: entityId, state: 'on' } as any), getAllEntities: () => ({ 'light.living_room': { entity_id: 'light.living_room', state: 'on' } } as any), callService: ({ domain, service }) => console.log(`Called ${domain}.${service}`), getStates: async () => [{ entity_id: 'light.living_room', state: 'on' }], getServices: async () => ({ light: { turn_on: {} } } as any), getConfig: async () => ({ location_name: 'Home' } as any), getUser: async () => ({ name: 'Test User' } as any), ready: true, lastUpdated: new Date(), }; export const Default: Story = { render: () => { const hass = useHass(); return ( <div style={{ padding: '16px' }}> <h3>useHass 返回值展示</h3> <div style={{ margin: '12px 0' }}> <p><strong>ready状态:</strong> {hass.ready ? '已就绪' : '未就绪'}</p> <p><strong>最后更新时间:</strong> {hass.lastUpdated.toLocaleString()}</p> <p><strong>连接状态:</strong> {hass.connection ? '已连接' : '未连接'}</p> </div> <div style={{ display: 'flex', gap: '8px', margin: '12px 0' }}> <button onClick={() => hass.callService({ domain: 'light', service: 'turn_on' })}> 调用light.turn_on服务 </button> <button onClick={async () => { const states = await hass.getStates(); alert(`获取到${states?.length || 0}个设备状态`); }}> 获取设备状态列表 </button> </div> </div> ); }, decorators: [ (Story) => ( <HassContext.Provider value={mockHassContext}> <Story /> </HassContext.Provider> ), ], parameters: { docs: { argsTable: { returns: [ { name: 'connection', type: { summary: 'Connection | null' }, description: '当前Home Assistant的连接实例,未连接时为null', }, { name: 'setConnection', type: { summary: '(connection: Connection) => void' }, description: '更新Home Assistant连接实例的方法', }, { name: 'getEntity', type: { summary: '(entity: string) => HassEntity' }, description: '根据实体ID查询单个设备/实体的详细信息', }, { name: 'getAllEntities', type: { summary: '() => HassEntities' }, description: '获取所有已注册的实体列表', }, { name: 'callService', type: { summary: '(args: CallServiceArgs) => void' }, description: '调用Home Assistant的服务,控制设备或执行操作', }, { name: 'getStates', type: { summary: '() => Promise<HassEntity[] | null>' }, description: '异步获取所有实体的当前状态', }, { name: 'getServices', type: { summary: '() => Promise<HassServices | null>' }, description: '异步获取Home Assistant支持的所有服务列表', }, { name: 'getConfig', type: { summary: '() => Promise<HassConfig | null>' }, description: '异步获取Home Assistant的配置信息', }, { name: 'getUser', type: { summary: '() => Promise<HassUser | null>' }, description: '异步获取当前登录用户的信息', }, { name: 'ready', type: { summary: 'boolean' }, description: '标记上下文是否已完成初始化,可安全使用', }, { name: 'lastUpdated', type: { summary: 'Date' }, description: '上下文数据最后更新的时间戳', }, ], }, }, }, };
关键说明
- 必须提供上下文模拟:因为
useHass依赖HassContext,如果没有Provider包裹,Hook会直接抛出错误,所以decorators是必不可少的。 - 手动补充文档是必要的:目前Storybook对Hook的自动ArgsTable生成支持较弱,尤其是返回值的详细说明,必须手动定义才能清晰展示每个字段的用途。
- 交互示例提升可读性:通过按钮等交互元素模拟调用Hook的方法,能让使用者快速理解Hook的实际功能,比纯文档更直观。
内容的提问来源于stack exchange,提问作者Shannon Hochkins
相关产品推荐
相关产品推荐

