深入掌握 Storybook Addon API:用 selectInCurrentKind 在组件内快速切换 Story
selectInCurrentKind 是 Storybook Addon API 中一个轻量而实用的导航方法,它允许开发者在已注册的 addon 中、仅凭一个 story 名称即可把当前界面切换到“当前组件(kind)”内的对应 story,无需关心组件的完整标题或 storyId。本文以 docs/_snippets/storybook-addons-api-selectincurrentkind.md 为骨架,结合 docs/addons/addons-api.mdx 的 API 文档与 code/core/src/manager-api/modules/stories.ts 的源码实现,完整讲解该 API 的用法、参数语义、底层导航原理及实战场景,读完即可在你的自定义 addon 中熟练使用。
一、selectInCurrentKind 是什么
在 Storybook 的 Addon API 体系中,selectInCurrentKind 属于“Storybook API”层的方法,与 api.selectStory() 同族。官方文档对其定位是:
Similar to the
selectStoryAPI method, but it only accepts the story as the only parameter.(与selectStory类似,但它只接收 story 名称这一个参数。)
也就是说:
selectStory允许你指定 kind(组件标题) 和可选的 story 名称,可在全局范围内跳转到任意 story;selectInCurrentKind则省略 kind 参数,只针对当前正在查看的组件,在其内部的 story 之间切换。
该 API 在 addon 的注册回调中通过传入的 api 实例调用,标准用法如下(即关联文档中的原始示例):
addons.register('my-organisation/my-addon', (api) => {
api.selectInCurrentKind('Default');
});
二、使用前提:在 addon 注册回调中获取 api 实例
要调用 selectInCurrentKind,必须先通过 addons.register() 拿到 Storybook API 实例。官方文档指出,addons.register() 是所有 addon 的入口,它允许你注册 addon 并访问 Storybook API(见 docs/addons/addons-api.mdx):
import { addons } from 'storybook/preview-api';
// Register the addon with a unique name.
addons.register('my-organisation/my-addon', (api) => {});
addons 通过 storybook/preview-api 包导入(参见 docs/_snippets/storybook-addons-api-imports.md)。在 register 回调的 api 参数上即可调用 selectInCurrentKind('StoryName')。
如果你的 addon 是带 UI 的组件(如 Panel、Toolbar),在组件内部也可以借助 useStorybookApi 钩子(来自 storybook/manager-api)获取同一套 API 实例,再调用导航方法。这种方式适合把“切换 story”动作绑定到按钮、下拉框等交互控件上。
三、与 selectStory 的对比:参数语义与适用场景
官方文档对 selectStory 的说明是:它接受两个参数——story kind 名称和可选的 story 名称。例如要选中 Button 组件下的 Default story(见 docs/_snippets/storybook-addons-api-selectstory.md):
addons.register('my-organisation/my-addon', (api) => {
api.selectStory('Button', 'Default');
});
两者对比如下:
| API | 参数 | 导航范围 | 适用场景 |
|---|---|---|---|
selectStory(kind, name?) |
kind 必填(可用组件标题或 storyId),name 可选 | 全局 | 跨组件跳转、跳转到指定 storyId、通过组件标题定位 |
selectInCurrentKind(name) |
仅 story 名称 | 当前组件(kind)内部 | 在同一组件不同变体/状态之间快速切换 |
从源码实现看,selectStory 在只传 name(即 titleOrId 为 undefined)时,其行为与 selectInCurrentKind 完全一致。在 code/core/src/manager-api/modules/stories.ts 中有明确的注释与实现:
} else if (!titleOrId) {
// Navigate to a named story/docs within the current component (i.e. "kind")
// This is a slugified version of the kind, but that's OK, our toId function is idempotent
gotoStory(hash[toId(kindSlug, name)]);
}
因此可以推断:selectInCurrentKind(name) 本质上就是 selectStory(undefined, name) 的便捷形式,省去了“获取当前组件 kind 再拼接”的样板代码。
四、源码级解析:它是如何在当前组件内完成导航的
要真正用好 selectInCurrentKind,理解其底层导航逻辑很有帮助。下面基于 code/core/src/manager-api/modules/stories.ts 的 selectStory 实现逐步拆解。
4.1 关键概念:kindSlug 与 storyId
在 Storybook 中,一个 story 的 ID 由 kind--story 的形式组成。源码中通过如下代码从当前 storyId 提取组件(kind)部分(stories.ts):
const kindSlug = storyId?.split('--', 2)[0];
selectInCurrentKind('Default') 导航时,会先把当前 story 的 storyId 按 -- 切分,取前半段作为组件标识,再定位到当前组件下名为 Default 的 story。
4.2 通过 toId 生成目标 storyId
定位目标 story 时,源码调用 toId(kindSlug, name) 来生成规范化的 storyId(stories.ts):
gotoStory(hash[toId(kindSlug, name)]);
toId 是 @storybook/csf 提供的工具函数(源码中通过 import { sanitize, toId } from 'storybook/internal/csf' 引入,并在模块中暴露为 storyId 属性,见 stories.ts)。它的特性是幂等(idempotent),即使 kind 传入的是已 slug 化的形式,也能得到相同结果。
4.3 最终导航:更新 URL 与记录状态
找到目标 entry 后,gotoStory 会更新 settings.lastTrackedStoryId 并通过 navigateWithQueryParams 将 URL 切换到 /{viewMode}/{entry.id}(stories.ts):
const gotoStory = (entry?: API_HashEntry) => {
if (entry?.type === 'docs' || entry?.type === 'story') {
store.setState({ settings: { ...settings, lastTrackedStoryId: entry.id } });
navigateWithQueryParams(
`/${entry.type}/${entry.refId ? `${entry.refId}_${entry.id}` : entry.id}${scrollTo ? `#${scrollTo}` : ''}`
);
return true;
}
return false;
};
由此可以看到,selectInCurrentKind 不仅切换了界面,还会同步更新 URL 与 Storybook 内部状态,因此刷新页面或分享链接时能保持选中的 story。
4.4 其他相关导航 API
jumpToStory(direction):在当前组件内按顺序跳转到上一个/下一个 story(stories.ts),与selectInCurrentKind一样属于“组件内导航”,区别在于它是相对位置导航而非按名称定位;selectFirstStory():选中第一个 story(stories.ts);- 跨 iframe 通信时,
SELECT_STORY事件(定义于 code/core/src/core-events/index.ts)会把kind/story/storyId参数转发给selectStory完成导航(stories.ts)。
五、实战:在自定义 addon 中实现“变体快捷切换”
假设你在开发一个组件测试辅助 addon,希望让用户在当前组件下快速切换不同的 story 变体(如 Default、Dark、RTL),可以这样实现:
import { addons } from 'storybook/preview-api';
addons.register('my-organisation/variant-switcher', (api) => {
// 在 Toolbar 或 Panel 的按钮点击事件中调用:
// 切换到当前组件下的 Dark story
api.selectInCurrentKind('Dark');
});
如果你在 Panel 组件内部使用 React 钩子,则可以写成:
import { useStorybookApi } from 'storybook/manager-api';
function VariantSwitcher() {
const api = useStorybookApi();
return (
<button onClick={() => api.selectInCurrentKind('Dark')}>
Switch to Dark variant
</button>
);
}
使用建议:
- story 名称要精确匹配:
selectInCurrentKind传入的名称需要与当前组件内 story 的导出名或name一致(底层通过toId拼接查找),拼写不一致会导致无法定位到目标; - 确保当前处于 story/docs 视图:若当前没有任何选中的 story,
kindSlug无法从storyId中提取,导航不会生效; - 与
getCurrentStoryData()配合:可以先调用api.getCurrentStoryData()读取当前 story 的kind、name、id等信息(见 docs/addons/addons-api.mdx),再做条件判断或动态拼接操作; - docs 页面同样适用:
gotoStory同时接受type === 'docs'的 entry,因此在 MDX 文档页之间也能用类似方式导航。
六、测试证据:行为已被单元测试锁定
Storybook 仓库在 code/core/src/manager-api/tests/stories.test.ts 中为 selectStory 维护了完整的单元测试(describe('selectStory'),见 stories.test.ts),其中直接覆盖了“在当前组件内按名称导航”的语义:
- 在当前组件内按给定名称导航(对应
selectInCurrentKind的语义):
it('allows navigating to a given name (in the current component)', () => {
const initialState = { path: '/story/a--1', storyId: 'a--1', viewMode: 'story' };
// ...
api.setIndex({ v: 5, entries: navigationEntries });
api.selectStory(undefined, '2');
expect(navigate).toHaveBeenCalledWith('/story/a--2', undefined);
});
- 按组件 id 跳转到第一个可见子 story:当过滤后的索引中第一个子 story 被隐藏时,会选择第一个可见的子项(stories.test.ts);
- 按组件标题(title)跳转:
api.selectStory('A')也能正确解析并导航到/story/a--1(stories.test.ts); - 记录最后跟踪的 story:导航后
settings.lastTrackedStoryId会被更新(stories.test.ts)。
这些测试表明:无论通过 selectStory 还是 selectInCurrentKind 触发组件内导航,最终都会统一收敛到 selectStory 的“按名称在当前 kind 内定位”分支,行为可预期、有回归保障。
七、小结
selectInCurrentKind 是 Storybook Addon API 中面向“组件内导航”的高频方法:它只接受一个 story 名称参数,自动基于当前组件完成 storyId 解析、URL 更新与状态记录。理解它与 selectStory 的关系(本质上是 selectStory(undefined, name) 的便捷封装)、底层 kindSlug + toId 的定位逻辑以及官方测试所锁定的行为边界,能帮助你在自定义 addon 中写出更稳健的导航功能。
延伸阅读
- Addon API 全量文档:docs/addons/addons-api.mdx
selectStory导航源码:code/core/src/manager-api/modules/stories.tsSELECT_STORY通道事件定义:code/core/src/core-events/index.tsselectStory行为测试:code/core/src/manager-api/tests/stories.test.ts
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00