首页
/ Storybook 组合组件 Story 中复用子组件 Story 数据:导入并渲染 ListItem 的 Unchecked Story 完整指南(CSF 3 与 CSF Next 多框架示例)

Storybook 组合组件 Story 中复用子组件 Story 数据:导入并渲染 ListItem 的 Unchecked Story 完整指南(CSF 3 与 CSF Next 多框架示例)

2026-09-07 15:23:20作者:翟萌耘Ralph

导读

在 Storybook 中,ListListItem 这类父子组件通常需要"组合渲染"才能展示有意义的界面。本篇文章聚焦于一个实用且易于维护的写法:在父组件(List)的 story 中直接导入子组件(ListItem.stories)里已定义好的某个 story(如 Unchecked),把它当作元素渲染出来,并通过展开其 args 复用子组件的输入数据。读完本文,你将掌握 CSF 3 与 CSF Next(实验特性)两套写法在 Angular、React、Solid、Vue 中的完整落地方案,理解 story 复用 story 的数据流原理,并能结合 subcomponents、children-as-arg、Template 组件等相邻模式做出取舍。本文以仓库文档片段 list-story-unchecked.md 为骨架展开。

这个代码片段在整个 Storybook 文档体系中扮演的角色

在官方文档中,"为一个复合组件(一次渲染两个及以上组件的组合)编写 stories"是编写 stories 的进阶课题,对应正文页面:

docs/_snippets/ 下的 list-story-*.md 七个文件正是上述正文页面通过 CodeSnippets 标签引用的多框架可运行代码片段:

片段文件 技术主题
list-story-starter.md 只渲染父组件本身的空 story
list-story-expanded.md 自定义渲染输出多个子组件
list-story-reuse-data.md 把子组件的多个 story args 拼入父 story 渲染
list-story-unchecked.md(本文主体) 把子组件某个具体 story(Unchecked)作为元素渲染进父 story,并复用其 args
list-story-with-unchecked-children.md 把渲染出的子组件放进 children arg
list-story-with-subcomponents.md subcomponents 属性联合文档化父子组件
list-story-template.md 创建"生成 story 的模板组件"

可以认为,list-story-unchecked.md 演示的是"单条子 story 直插父组件"的最小复用手法,list-story-reuse-data.md 则是它在"多条数据、多个子 story"场景下的推广。二者在 docs/writing-stories 系列页面中共同构成了 story 之间数据复用的最佳实践基线

核心模式:story 导入 story

List 的 story 里写死 ListItem 的输入数据(例如手动写 item={{ title: '...', checked: false }}),会让同一份数据散落在 ListItem.stories.tsList.stories.ts 两处,任何改动都需要同步维护多处。而 list-story-unchecked.md 给出的模式只有三步:

  1. 导入子组件的 stories 文件import { Unchecked } from './ListItem.stories';
  2. 在父 story 中把 Unchecked 当作组件元素渲染(由各框架的 render 函数完成);
  3. 通过展开 Unchecked.args 复用子 story 已经定义好的输入数据

这样"数据定义只存在于 ListItem.stories 一处",List 的 story 只需要表达"我包含一个处于 uncheck 状态的 ListItem"这一组合意图。

从 Storybook 数据流看,story 的 args 本质就是一段会被传入组件渲染逻辑的输入;当一个 story 对象作为"数据源"被另一个 story 引用时,子 story 的 args 通过对象展开被摊平到父 story 的渲染上下文中。父组件接收到的 props 与渲染出的子组件收到的 props 因此始终一致,从而保证 Controls、Actions 等基于 args 的机制看到的数据同源。

Angular:moduleMetadata + 渲染函数返回模板

Angular 组合渲染的差异点在于:模板中的自定义标签(<app-list><app-list-item>)必须经由 moduleMetadata 装饰器完成组件声明,渲染函数则返回一个描述模板的对象,并通过 props: args 把 args 桥接进模板作用域。

CSF 3 写法

import { type Meta, type StoryObj, moduleMetadata } from '@storybook/angular';

import { CommonModule } from '@angular/common';

import { List } from './list.component';
import { ListItem } from './list-item.component';

// Imports a specific story from ListItem stories
import { Unchecked } from './ListItem.stories';

const meta: Meta<List> = {
  component: List,
  decorators: [
    moduleMetadata({
      declarations: [List, ListItem],
      imports: [CommonModule],
    }),
  ],
};

export default meta;
type Story = StoryObj<List>;

export const OneItem: Story = {
  render: (args) => ({
    props: args,
    template: `
      <app-list>
        <app-list-item [item]="item"></app-list-item>
      </app-list>
   `,
  }),
  args: {
    ...Unchecked.args,
  },
};

要点拆解:

  • moduleMetadata 必须在组件级或 story 级注入,declarations 需要同时包含 List 与其模板中用到的 ListItem,否则 Angular 编译器会因为模板中出现未知标签而报错;
  • render 函数返回的模板对象中,props: args 等价于把 story 的 args 放入组件模板上下文,因此 [item]="item" 绑定的 item 正是由 ...Unchecked.args 提供的属性(Unchecked story 定义里包含 item 输入);
  • 这样设置后,Controls 面板能直接编辑父 story 的 item 输入,且修改只发生在这一个数据源上。

CSF Next(实验特性)写法

import { CommonModule } from '@angular/common';

import { moduleMetadata } from '@storybook/angular';

import preview from '../.storybook/preview';

import { List } from './list.component';
import { ListItem } from './list-item.component';

// Imports a specific story from ListItem stories
import { Unchecked } from './ListItem.stories';

const meta = preview.meta({
  component: List,
  decorators: [
    moduleMetadata({
      declarations: [List, ListItem],
      imports: [CommonModule],
    }),
  ],
});

export const OneItem = meta.story({
  render: (args) => ({
    props: args,
    template: `
      <app-list>
        <app-list-item [item]="item"></app-list-item>
      </app-list>
   `,
  }),
  args: {
    ...Unchecked.input.args,
  },
});

CSF Next 不再使用 export default meta + satisfies Meta<typeof List> 的手动类型声明,而是经由 preview.meta({...}) 返回带完整类型推断的 meta,再用 meta.story({...}) 产出 story。组件类型、args 类型全部沿工厂链自动推导。可参考仓库中的 docs/api/csf/csf-next.mdx 说明:definePreview → preview.meta → meta.story 三个工厂逐级产生下一个函数,每一级都保留类型安全。

React:渲染函数直接返回 JSX

React 系(React 与 Solid)是组合渲染最直接的一类:render 函数本身就是组件渲染,把导入的 Unchecked 当作普通 React 元素嵌套进 List 即可。

CSF 3 —— React/JavaScript

import { List } from './List';

// Instead of importing ListItem, we import the stories
import { Unchecked } from './ListItem.stories';

export default {
  component: List,
};

export const OneItem = {
  render: (args) => (
    <List {...args}>
      <Unchecked {...Unchecked.args} />
    </List>
  ),
};

CSF 3 —— React/TypeScript

// 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';

export const meta = {
  component: List,
} satisfies Meta<typeof List>;

export default meta;
type Story = StoryObj<typeof meta>;

export const OneItem: Story = {
  render: (args) => (
    <List {...args}>
      <Unchecked {...Unchecked.args} />
    </List>
  ),
};

CSF 3 —— Solid/JavaScript

import { List } from './List';

// Instead of importing ListItem, we import the stories
import { Unchecked } from './ListItem.stories';

export default {
  component: List,
};

export const OneItem = {
  render: (args) => (
    <List {...args}>
      <Unchecked {...Unchecked.args} />
    </List>
  ),
};

CSF 3 —— Solid/TypeScript

import type { Meta, StoryObj } from 'storybook-solidjs-vite';

import { List } from './List';

// Instead of importing ListItem, we import the stories
import { Unchecked } from './ListItem.stories';

export const meta = {
  component: List,
} satisfies Meta<typeof List>;

export default meta;
type Story = StoryObj<typeof meta>;

export const OneItem: Story = {
  render: (args) => (
    <List {...args}>
      <Unchecked {...Unchecked.args} />
    </List>
  ),
};

注意两个容易踩的细节:

  1. <List {...args}><Unchecked {...Unchecked.args} /> 是两层独立展开args 传给父组件 List,而 Unchecked.args 传给被渲染的子 story 元素;之所以在 JSX 里引用 story 而非原始组件,是为了"数据"和"渲染表现"(比如 Unchecked 状态下绑定的 handler)都从子 story 继承;
  2. React 写法中也可以省略显式 render,改为把子元素放进 children arg——那是 children-as-arg 变体(见后文 list-story-with-unchecked-children.md),当前写法则把组合关系锁定在 render 函数内,父组件收到的仍是普通 props。

CSF Next —— React/TypeScript 与 JavaScript

import preview from '../.storybook/preview';

import { List } from './List';

// Instead of importing ListItem, we import the stories
import { Unchecked } from './ListItem.stories';

const meta = preview.meta({
  component: List,
});

export const OneItem = meta.story({
  render: (args) => (
    <List {...args}>
      <Unchecked {...Unchecked.input.args} />
    </List>
  ),
});
import preview from '../.storybook/preview';

import { List } from './List';

// Instead of importing ListItem, we import the stories
import { Unchecked } from './ListItem.stories';

const meta = preview.meta({
  component: List,
});

export const OneItem = meta.story({
  render: (args) => (
    <List {...args}>
      <Unchecked {...Unchecked.input.args} />
    </List>
  ),
});

(注:仓库片段中 CSF Next 的 React TS 与 JS 代码体完全一致,差异仅在文件类型;两者都保留的原因见片段中的 JS snippets still needed while providing both CSF 3 & Next 注释。)

Vue 3:render 返回组件与模板描述

Vue 3 的 render 函数需要返回 { components, setup, template } 描述对象:components 注册 ListListItemsetup() 把 args 暴露给模板,模板中通过 v-bind="args"Unchecked story 的数据注入 ListItem

CSF 3 —— JavaScript

import List from './List.vue';
import ListItem from './ListItem.vue';

// Imports a specific story from ListItem stories
import { Unchecked } from './ListItem.stories';

export default {
  component: List,
};

export const OneItem = {
  args: {
    ...Unchecked.args,
  },
  render: (args) => ({
    components: { List, ListItem },
    setup() {
      // The args will now be passed down to the template
      return { args };
    },
    template: '<List v-bind="args"><ListItem v-bind="args" /></List>',
  }),
};

CSF 3 —— TypeScript

import type { Meta, StoryObj } from '@storybook/vue3-vite';

import List from './List.vue';
import ListItem from './ListItem.vue';

// 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>;

export const OneItem: Story = {
  render: (args) => ({
    components: { List, ListItem },
    setup() {
      // The args will now be passed down to the template
      return { args };
    },
    template: '<List v-bind="args"><ListItem v-bind="args" /></List>',
  }),
  args: {
    ...Unchecked.args,
  },
};

Vue 版本的关键在于 <ListItem v-bind="args" />:这里 v-bind="args" 以展开方式把 story 的 args 全部绑定为子组件 props。由于 args 里已包含 Unchecked.args 展开出的字段,父组件 List 与子组件 ListItem 会接收到同一份数据,Controls 修改其中一个都会即时反映到组合界面上。

CSF Next —— TypeScript、JavaScript

import preview from '../.storybook/preview';

import List from './List.vue';
import ListItem from './ListItem.vue';

// Imports a specific story from ListItem stories
import { Unchecked } from './ListItem.stories';

const meta = preview.meta({
  component: List,
});

export const OneItem = meta.story({
  render: (args) => ({
    components: { List, ListItem },
    setup() {
      // The args will now be passed down to the template
      return { args };
    },
    template: '<List v-bind="args"><ListItem v-bind="args" /></List>',
  }),
  args: {
    ...Unchecked.input.args,
  },
});
import preview from '../.storybook/preview';

import List from './List.vue';
import ListItem from './ListItem.vue';

// Imports a specific story from ListItem stories
import { Unchecked } from './ListItem.stories';

const meta = preview.meta({
  component: List,
});

export const OneItem = meta.story({
  render: (args) => ({
    components: { List, ListItem },
    setup() {
      // The args will now be passed down to the template
      return { args };
    },
    template: '<List v-bind="args"><ListItem v-bind="args" /></List>',
  }),
  args: {
    ...Unchecked.input.args,
  },
});

CSF 3 与 CSF Next 在"数据复用"上的关键差异:.args 还是 .input.args

对比两套写法不难发现复用子 story 数据时的取数路径不同:

  • CSF 3:直接访问 Unchecked.args
  • CSF Next:访问 Unchecked.input.args

为什么?仓库的 CSF Next 迁移文档 docs/api/csf/csf-next.mdx(3.1 Reusing story properties 小节)解释了这一点:CSF Next 中直接读取 Story.argsStory.parameters 等属性虽然仍被支持,但已属弃用用法。story 的全部属性被收纳进新属性 composedStory.composed.args / Story.composed.parameters),之所以叫 "composed",是因为其中的值是由 story、其组件 meta 以及 preview 全局配置共同组合(compose)出来的;如果只想要"直接喂给这个 story 的原始输入",则应读取 Story.input

因此,在父 story 中引用一个尚未经过完整组合的子 story 时,代码片段统一使用 Unchecked.input.args 来取得 Unchecked 的原始 args 输入,再通过展开合成到 OneItem 自己的 args 中——这与 meta.story() 工厂产出的新对象模型保持一致,也意味着一旦子 story 后续经过 .extend() 等 CSF Next 组合能力调整,父 story 读取到的仍是稳定的原始输入层。如果你的项目全部采用 CSF Next,还可以进一步关注 .composedStory.extend(复用 story 属性时官方推荐 .extend)以做更复杂的组合。

与其他"多组件 story"写法的取舍

list-story-unchecked.md 并不是多组件 story 的唯一答案。把它放入 stories-for-multiple-components.mdx 的语境看,可以得出如下选型参考:

  • subcomponents(list-story-with-subcomponents.md:当子组件"不单独使用、只作为父组件的一部分"时,在 meta 上声明 subcomponents,可在 ArgTypes/Controls 文档表里额外展示子组件 props。但 subcomponents 仅用于文档化:其 argTypes 只能靠自动推断、不可手动覆盖,且子组件表格不提供 controls(controls 永远作用于主组件 args)。
  • 本文件演示的 story 复用 + render(unchecked 版本):通过把子 story 作为数据源渲染进父 story,减少重复、且能通过父组件渲染控制组合结构;但父 story 的数据依然由父组件的 args 主导,子 story 本身不能被单独 controls。仓库 docs/writing-stories/index.mdx 在引出这组写法时也特别提示:以这种方式编写复合组件 story,无法充分发挥 args 机制与后续组合 args 的能力(对应 index.mdx 中 "Stories for two or more components" 的 Callout),更复杂时建议转向 children-as-arg 或 Template 组件方案。
  • children as an arg(list-story-with-unchecked-children.md:把渲染出的 <Unchecked {...Unchecked.args} /> 放进 children arg,使子元素成为可复用、可跨 story 引用的数据。代价是 children arg 与所有 args 一样必须 JSON 可序列化:应避免空值、若需用 controls 微调值应使用 mapping、对含第三方库的组件需谨慎。
  • Template 组件(list-story-template.md:把组合逻辑封装成"story 生成器"模板组件,更偏"数据驱动",需要更多初始化代码,但每个子 story 都能保留自己的 args 并接受 Controls 面板编辑。

实际工程中可参考这条决策线:仅文档化 → subcomponents;单条简单复用 → 本文的 story-import-story;需要 controls 深入编辑子数据 → children arg / Template 组件

小结

  • 把子组件的 story 当作数据与表现的双重来源:import { Unchecked } from './ListItem.stories' 后在父 story 的 render 中渲染它并 ...Unchecked.args,数据只需维护一处;
  • Angular 需 moduleMetadata 声明父子组件并借助 props: args 把 args 暴露给模板;React/Solid 在 render 里直接返回 JSX;Vue 3 在 render 返回对象中通过 components + setup + template 组织渲染;
  • 从 CSF 3 迁移到 CSF Next 时,复用子 story 的 args 从 Unchecked.args 改为 Unchecked.input.args,背后的 .composed / .input 语义与 meta.story() 工厂链保持一致(详见 docs/api/csf/csf-next.mdx);
  • 更多上下文与相邻模式见 docs/writing-stories/index.mdxdocs/writing-stories/stories-for-multiple-components.mdx 正文,完整的多框架对照可继续查看 docs/_snippets/list-story-*.md 系列片段。
登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
915
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388