文档
Storybook Docs

多个组件的 Stories

编写一次渲染两个或多个组件的故事非常有用,前提是这些组件被设计为协同工作。例如,ButtonGroupListPage 组件。

子组件

当您要记录的组件具有父子关系时,您可以使用subcomponents属性将它们一起记录。当子组件不打算单独使用,而仅作为父组件的一部分使用时,这一点尤其有用。

这是一个使用 ListListItem 组件的示例

List.stories.ts|tsx
import * as React from 'react';
 
// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, nextjs-vite, etc.
import type { Meta, StoryObj } from '@storybook/your-framework';
 
import { List } from './List';
import { ListItem } from './ListItem';
 
const meta = {
  component: List,
  subcomponents: { ListItem }, //👈 Adds the ListItem component as a subcomponent
} satisfies Meta<typeof List>;
export default meta;
 
type Story = StoryObj<typeof meta>;
 
export const Empty: Story = {};
 
export const OneItem: Story = {
  render: (args) => (
    <List {...args}>
      <ListItem />
    </List>
  ),
};

请注意,通过向默认导出添加 subcomponents 属性,我们在ArgTypesControls 表中获得了一个额外的面板,其中列出了 ListItem 的 props

Subcomponents in ArgTypes doc block

子组件仅用于文档目的,并且有一些限制

  1. 子组件的argTypes推断的(对于支持此功能的渲染器),不能手动定义或覆盖。
  2. 为每个记录的子组件提供的表格包含控件来更改 props 的值,因为控件始终应用于主组件的 args。

让我们讨论一些您可以用来缓解上述问题的技术,这些技术在更复杂的情况下特别有用。

重用故事定义

我们还可以通过重用故事定义来减少故事中的重复。在这里,我们可以重用 ListItem 故事的 args 在 List 的故事中

List.stories.ts|tsx
import * as React from 'react';
 
// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, nextjs-vite, etc.
import type { Meta, StoryObj } from '@storybook/your-framework';
 
import { List } from './List';
import { ListItem } from './ListItem';
 
//👇 We're importing the necessary stories from ListItem
import { Selected, Unselected } from './ListItem.stories';
 
const meta = {
  component: List,
} satisfies Meta<typeof List>;
 
export default meta;
type Story = StoryObj<typeof meta>;
 
export const ManyItems: Story = {
  render: (args) => (
    <List {...args}>
      <ListItem {...Selected.args} />
      <ListItem {...Unselected.args} />
      <ListItem {...Unselected.args} />
    </List>
  ),
};

通过渲染带有其 args 的 Unchecked 故事,我们可以重用 ListItem 故事中的输入数据到 List 中。

但是,我们仍然没有使用 args 来控制 ListItem 故事,这意味着我们无法通过 controls 更改它们,也无法在其他更复杂的组件故事中重用它们。

将子组件用作 arg

改进这种情况的一种方法是将渲染出的子组件提取到一个 children arg 中

List.stories.ts|tsx
// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, nextjs-vite, etc.
import type { Meta, StoryObj } from '@storybook/your-framework';
 
import { List } from './List';
 
//👇 Instead of importing ListItem, we import the stories
import { Unchecked } from './ListItem.stories';
 
const meta = {
  component: List,
} satisfies Meta<typeof List>;
 
export default meta;
type Story = StoryObj<typeof meta>;
 
export const OneItem: Story = {
  args: {
    children: <Unchecked {...Unchecked.args} />,
  },
};

现在 children 是一个 arg,我们可以在另一个故事中潜在地重用它。

但是,使用这种方法有一些需要注意的细节。

与所有 arg 一样,children arg 需要是 JSON 可序列化的。为避免 Storybook 出错,您应该

  • 避免使用空值
  • 使用映射如果您想使用控件来调整值
  • 谨慎使用包含第三方库的组件

我们目前正在改进 children arg 的整体体验,并允许您在不久的将来在控件中编辑 children arg,并允许您使用其他类型的组件。但目前,您在实现您的故事时需要考虑这个注意事项。

创建模板组件

另一个更“基于数据”的选项是创建一个特殊的“故事生成”模板组件

List.stories.ts|tsx
// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, nextjs-vite, etc.
import type { Meta, StoryObj } from '@storybook/your-framework';
 
import { List } from './List';
import { ListItem } from './ListItem';
 
//👇 Imports a specific story from ListItem stories
import { Unchecked } from './ListItem.stories';
 
const meta = {
  component: List,
} satisfies Meta<typeof List>;
 
export default meta;
type Story = StoryObj<typeof meta>;
 
//👇 The ListTemplate construct will be spread to the existing stories.
const ListTemplate: Story = {
  render: ({ items, ...args }) => {
    return (
      <List>
        {items.map((item) => (
          <ListItem {...item} />
        ))}
      </List>
    );
  },
};
 
export const Empty = {
  ...ListTemplate,
  args: {
    items: [],
  },
};
 
export const OneItem = {
  ...ListTemplate,
  args: {
    items: [{ ...Unchecked.args }],
  },
};

这种方法设置起来有点复杂,但它意味着您可以更轻松地重用复合组件中每个故事的args。它也意味着您可以通过控件面板来修改组件的 args。