FEATURED · 精选文章

Storybook 组件故事中使用 React Hooks 与 Solid Signals:在 render 函数内维护组件内部状态

发布时间 / 2026/9/18 2:51:55
来源 / 创域科博编辑部
栏目 / 资讯中心
Storybook 组件故事中使用 React Hooks 与 Solid Signals:在 render 函数内维护组件内部状态 Storybook 组件故事中使用 React Hooks 与 Solid Signals在 render 函数内维护组件内部状态导读在 Storybook 的 Component Story FormatCSF中一个 story故事本质上是组件在给定一组参数args下应如何渲染的一份带注解的对象。大多数场景用args即可描述状态但当你要展示依赖组件内部状态如点击后切换按钮外观的行为时就需要在render函数中借用响应式能力。本文以仓库文档中的Button示例为骨架完整给出 ReactuseState与 SolidcreateSignal在 CSF 3 与 CSF Next 两代写法中的实践并讲解为什么官方默认推荐用args而非 Hooks帮助你写出既正确又易于后续维护的故事文件。本文对应的原文档片段位于 docs/_snippets/button-story.md它被正式文档 docs/writing-stories/index.mdx 的 Working with React Hooks / Solid Signals 章节引用。从纯 args到需要内部状态的故事场景先回顾 Storybook 对 story 的定义一个 story 捕获 UI 组件的某种渲染状态它是对组件在给定参数下行为与外观的注解对象见 docs/writing-stories/index.mdx 开头。仓库中配套的组件定义 docs/_snippets/button-implementation.md 给出了一个典型Buttonexport interface ButtonProps { /** Is this the principal call to action on the page? */ primary?: boolean; /** What background color to use */ backgroundColor?: string; /** How large should the button be? */ size?: small | medium | large; /** Button contents */ label: string; /** Optional click handler */ onClick?: () void; }这个Button是受控组件primary、label由外部传入。默认情况下story 会渲染meta即默认导出中声明的component并把args作为 props 传入。因此在绝大多数场景里你只需通过 args 描述按钮处于 primary 还是 secondary 状态例如export default { component: Button, }; export const Primary { args: { primary: true, label: Button, }, };这种写法使故事成为组件的一个纯粹渲染快照也便于 Controls 面板在运行期实时修改参数、便于其它故事通过对象展开复用这正是官方在文档中强烈建议优先使用 args的原因。然而如果你想演示点击按钮后从 Secondary 变为 Primary这类依赖交互改变状态的动态行为而该状态又不在组件 props 契约内就可以借助框架自身的响应式 API在render函数中构建一个带内部状态的包装组件。推荐用 args 描述绝大多数故事在深入 Hooks 写法前需要明确docs/writing-stories/index.mdx中把 Hooks/Signals 明确标注为advanced use case进阶用例同时给出的建议是尽可能使用 args 来编写你的故事。两种方式的对比与选型可以归纳为场景推荐做法描述组件的某个外观/状态快照在 story 上声明args纯数据可在 Controls 中实时编辑展示点击后状态切换等内部状态变化在render函数中用 React Hooks / Solid Signals 包一层需要在 story 之外复用渲染逻辑把render函数提升到meta层供多个 story 共享render函数可以出现在两个层级放在meta中可被多个 story 复用放在某个 story 对象中则只作用于该 storystory 层的定义会覆盖 meta 层的定义见 docs/writing-stories/index.mdx 中 Custom rendering 章节。无论放在哪一层render都会收到 args 作为第一个参数因此内部实现通常会通过展开运算符把args透传给组件保证 Controls 等依赖 args 的特性仍然生效并可通过第二个context参数访问parameters、globals等故事上下文。React 场景在 render 中用 useState 驱动按钮切换原文档片段给出了一份用 Hooks 改变按钮状态的完整示例。它的核心思路是在 story 的render中创建一个内部组件ButtonWithHooks用useState同时维护label与isPrimary两个状态再通过一个点击处理器同步修改二者从而实现首次点击后按钮从 Secondary 变为 Primary 的动态效果。CSF 3JavaScriptimport React, { useState } from react; import { Button } from ./Button; export default { component: Button, }; const ButtonWithHooks () { // Sets the hooks for both the label and primary props const [value, setValue] useState(Secondary); const [isPrimary, setIsPrimary] useState(false); // Sets a click handler to change the labels value const handleOnChange () { if (!isPrimary) { setIsPrimary(true); setValue(Primary); } }; return Button primary{isPrimary} onClick{handleOnChange} label{value} /; }; export const Primary { render: () ButtonWithHooks /, };要点拆解value按钮文字与isPrimary是否主按钮样式构成两个独立状态初始值分别为Secondary与falsehandleOnChange在非 primary 时一次性把两者都切到 primary 状态if (!isPrimary)起到幂等保护后续点击不会再改写状态Primary故事没有使用args而是通过render返回ButtonWithHooks /让 Hooks 在渲染期间自行管理组件实例的状态。由于状态封装在渲染树内部Storybook 在渲染该故事时会对Button进行真实的点击交互并观察到 UI 变化——这正是此类示例用于演示而非断言的定位。CSF 3TypeScript satisfies在 TypeScript 项目中推荐从框架入口引入Meta、StoryObj泛型并使用 TS 4.9 的satisfies操作符让类型在编译期严格校验import React, { useState } 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 { Button } from ./Button; const meta { component: Button, } satisfies Metatypeof Button; export default meta; type Story StoryObjtypeof meta; const ButtonWithHooks () { // Sets the hooks for both the label and primary props const [value, setValue] useState(Secondary); const [isPrimary, setIsPrimary] useState(false); // Sets a click handler to change the labels value const handleOnChange () { if (!isPrimary) { setIsPrimary(true); setValue(Primary); } }; return Button primary{isPrimary} onClick{handleOnChange} label{value} /; }; export const Primary { render: () ButtonWithHooks /, } satisfies Story;satisfies Metatypeof Button让component必须与真实组件类型匹配防止把错误的组件声明进元信息satisfies Story作用于导出对象确保render、args等字段符合 Storybook 对该 story 的约束type Story StoryObjtypeof meta让 story 的args类型自动与组件 props 对齐。TypeScript 的零配置支持与泛型推导细节可参考 docs/writing-stories/typescript.mdx。Solid 场景用 createSignal 等价实现在原文档中同样的示例为 Solid 用户提供了基于Solid Signals的变体。Solid 用createSignal替代useState读取值需通过函数调用value()/isPrimary()写入仍用 setter。核心状态机逻辑与 React 版本完全一致仅响应式 API 形态不同import { createSignal } from solid-js; import { Button } from ./Button; export default { component: Button, }; const ButtonWithHooks () { // Sets the signals for both the label and primary props const [value, setValue] createSignal(Secondary); const [isPrimary, setIsPrimary] createSignal(false); // Sets a click handler to change the labels value const handleOnChange () { if (!isPrimary()) { setIsPrimary(true); setValue(Primary); } }; return Button primary{isPrimary()} onClick{handleOnChange} label{value()} /; }; export const Primary { render: () ButtonWithHooks /, };import type { Meta, StoryObj } from storybook-solidjs-vite; import { createSignal } from solid-js; import { Button } from ./Button; const meta { component: Button, } satisfies Metatypeof Button; export default meta; type Story StoryObjtypeof meta; const ButtonWithHooks () { // Sets the signals for both the label and primary props const [value, setValue] createSignal(Secondary); const [isPrimary, setIsPrimary] createSignal(false); // Sets a click handler to change the labels value const handleOnChange () { if (!isPrimary()) { setIsPrimary(true); setValue(Primary); } }; return Button primary{isPrimary()} onClick{handleOnChange} label{value()} /; }; export const Primary { render: () ButtonWithHooks /, } satisfies Story;对 Solid 读者需要特别强调两点Signal 读取必须加括号判断用isPrimary()传值用primary{isPrimary()}Solid 的细粒度响应式意味着每次状态写入只重渲染依赖该 signal 的 JSX 节点因此该示例在交互上比 React 版本更精准但书写约定仍与 CSF 完全一致。CSF Next改用 preview.meta 与 meta.story 的工厂写法仓库文档如 docs/_snippets/button-story.md在展示 React 示例时同时提供了CSF Next preview标签页它是一种仍在预览阶段的下一版 CSF 规范完整参考见 docs/api/csf/csf-next.mdx。其 API 由三个工厂函数串成一条类型安全链路definePreview在 .storybook/preview.* 中→ preview.meta定义组件元信息→ meta.story定义故事首先把项目级 preview 配置从对象字面量改为definePreview({ addons: [...] })addon 提供的注解类型会在全项目内被推断出来故事文件不再从storybook/framework导入Meta/StoryObj而是import preview from ../.storybook/previewmeta不需要再作为 default export也不用手写satisfies—— 工厂链的类型在每一步自动推导。React 的 CSF Next 版本把上文的 meta 与 story 两部分分别交给preview.meta与meta.storyimport React, { useState } from react; import preview from ../.storybook/preview; import { Button } from ./Button; const meta preview.meta({ component: Button, }); const ButtonWithHooks () { const [value, setValue] useState(Secondary); const [isPrimary, setIsPrimary] useState(false); const handleOnChange () { if (!isPrimary) { setIsPrimary(true); setValue(Primary); } }; return Button primary{isPrimary} onClick{handleOnChange} label{value} /; }; export const Primary meta.story({ render: () ButtonWithHooks /, });以及对应的 JavaScript 版本import React, { useState } from react; import preview from ../.storybook/preview; import { Button } from ./Button; const meta preview.meta({ component: Button, }); const ButtonWithHooks () { const [value, setValue] useState(Secondary); const [isPrimary, setIsPrimary] useState(false); const handleOnChange () { if (!isPrimary) { setIsPrimary(true); setValue(Primary); } }; return Button primary{isPrimary} onClick{handleOnChange} label{value} /; }; export const Primary meta.story({ render: () ButtonWithHooks /, });注意两点与渲染逻辑相关的差异render语义不变无论哪种 CSF 版本render函数都返回实际渲染的内容。因此在 CSF Next 里Hooks 包装组件的用法与 CSF 3 完全一致类型推导能力增强在 CSF Next 中你不再需要Metatypeof Button手动声明preview.meta({ component: Button })即可让meta.story({ render: (args) ... })的args具备组件 props 的完整类型复杂自定义类型可交给preview.type{ args: CustomProps }()显式扩展。仓库自身的组件库同样使用这种工厂写法可参见 code/addons/a11y/src/components/A11YPanel.stories.tsx它从../../../../.storybook/preview.tsx导入preview用preview.meta({ title: Panel, component: A11YPanel, parameters: { layout: fullscreen } })定义 meta再通过meta.story({ render, play })声明大量组件状态 story。这从源码层面印证了 CSF Next 工厂链的真实形态。三类写法的取舍与注意事项优先 argsHooks 仅在必要时使用原文档的上下文docs/writing-stories/index.mdx 中 Working with React Hooks / Solid Signals 小节明确把 Hooks/Signals 标为 advanced use case并建议以 args 为主。原因在于 args 写法让故事可被 Controls 实时编辑、可被其它故事复用如组合出 ButtonGroup 故事且不依赖渲染期间的 React/Solid 运行时行为。Hooks 版本适用场景当组件是受控组件而你希望演示纯前端内部状态的流转且这些状态不适合外泄为 args 时再使用render 响应式状态。渲染函数需要可预测render会在每次故事渲染时执行不要在渲染函数顶层做有副作用的分支逻辑React 对 Hooks 调用顺序有严格约束务必将所有useState/createSignal放在条件判断之前示例中固定的三行状态声明顺序即为此服务。注意向组件透传 args若在render函数里既想用 Hooks 又想保留 Controls 能力应在自定义渲染中把args展开传给组件官方文档将其列为 custom rendering 的最佳实践参见 docs/writing-stories/index.mdx Custom rendering 章节。CSF Next 是预览特性据 docs/api/csf/csf-next.mdxCSF Next 目前处于 preview 阶段仅面向 React、Vue、Angular 与 Web Components 项目API 存在调整可能同一文件内不可混用不同 CSF 版本。小结为 Storybook 编写带交互动态的故事路径清晰且高度依赖框架自身的响应式设施默认用args描述故事让组件保持可测试、可复用、可在 Controls 面板中即席调参需要演示组件内部状态切换时在 story 的render函数内创建带useState/createSignal的包装组件即 docs/_snippets/button-story.md 的完整示范注意保持状态声明顺序稳定若项目已采用类型安全优先的新一代 API则把这段逻辑迁移到preview.meta({ component })与meta.story({ render })的工厂链中省略手写泛型与satisfies让类型沿definePreview → preview.meta → meta.story逐级自动推导。无论选择 CSF 3 还是 CSF Nextrender永远是你按需接管组件渲染方式的扩展点理解何时用 args、何时进 render是写出高质量、贴近真实组件行为的故事文件的关键一步。创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻