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

如何为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的实际使用效果。

具体实现步骤

  1. 模拟上下文环境
    用Storybook的decorators为所有Story包裹模拟的HassContext.Provider,提供预先定义好的上下文数据,确保Hook能正常运行。

  2. 编写展示组件
    在Story的render函数中创建一个测试组件,调用useHass并将返回值的属性、方法可视化展示,甚至添加交互按钮来模拟调用Hook的方法,让用户直观看到Hook的功能。

  3. 手动补充文档
    在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.18 00:24:56