首页
/ Storybook 插件全局状态联动实战:useGlobals 与 updateGlobals 深入解析

Storybook 插件全局状态联动实战:useGlobals 与 updateGlobals 深入解析

2026-09-08 11:36:07作者:田桥桑Industrious

useGlobals 是 Storybook 面向插件(Addon)开发者的核心 Hook,它让运行在管理器(Manager)界面中的插件 UI 能够读取并更新 Storybook 的全局状态(Globals),从而驱动工具栏、面板、装饰器等跨 story 共享的交互状态。本文以 storybook-addons-api-useglobal.md 示例为骨架,结合 manager-apipreview-api 的源码实现,讲解如何用该 Hook 读写 Globals、理解其事件闭环原理,并规避高频重渲染问题。读完你将掌握一个可直接落地的插件面板开关方案,并能读懂 Storybook 管理端与预览端同步全局状态的底层调用链。

定位:为何插件 UI 需要读写 Globals

在开始写代码之前,先厘清这个 Hook 在 Addon API 生态中的位置。官方 API 参考文档 addons-api.mdx 中明确说明:

useGlobals —— Extremely useful hook for addons that rely on Storybook Globals. It allows you to obtain and update global values.

Globals 与单篇 story 的 args 不同:args 属于某一条具体 story,而 globals 是项目级、跨 story 的全局值(例如主题明暗、语言 locale、viewport 之类),它们通常由工具栏、URL 查询参数或 story 级覆盖共同决定。插件如果依赖这类全局状态(例如背景色插件读取当前主题、或一个面板需要根据某个全局开关显隐),就需要在管理器侧订阅并修改它,这正是 useGlobals 的职责。

addons-api.mdx 中,useGlobalsuseChanneluseAddonStateuseParameteruseArgs 等并列,属于插件可用的 React Hooks 集合。它们全部需要从 storybook/manager-api 导入,即用于“与 Storybook 管理器 UI 交互或访问 Storybook API”的那一层(另一包 storybook/preview-api 则用于控制插件的运行行为)。这一点决定了本文示例的使用场景:在 manager 侧渲染的面板、工具栏、Tab 组件中读取与修改全局值

完整示例:用 useGlobals 做一个可切换显隐的面板

以下是 storybook-addons-api-useglobal.md 中给出的完整可运行示例(源文件位于 docs/_snippets/storybook-addons-api-useglobal.md,文件名标注为 my-addon/manager.js|ts,适用于 JS 与 TS 两种写法):

import React from 'react';

import { AddonPanel, Button } from 'storybook/internal/components';

import { useGlobals } from 'storybook/manager-api';

export const Panel = () => {
  const [globals, updateGlobals] = useGlobals();

  const isActive = globals['my-param-key'] || false; // 👈 Sets visibility based on the global value.

  return (
    <AddonPanel key="custom-panel" active={isActive}>
      <Button onClick={() => updateGlobals({ ['my-param-key']: !isActive })}>
        {isActive ? 'Hide the addon panel' : 'Show the panel'}
      </Button>
    </AddonPanel>
  );
};

这段代码虽短,但包含了 useGlobals 的全部核心用法,逐行拆解如下:

  • 导入来源useGlobalsstorybook/manager-api 导入;面板容器与按钮从 storybook/internal/components 导入。注意这段代码属于插件“管理端”代码,它运行在 Storybook 的 manager(主界面进程)中,而不是在渲染 story 的 iframe 预览区内。
  • 解构取值const [globals, updateGlobals] = useGlobals();。第一个返回值是当前全局状态对象(形如 { 'my-param-key': true } 的字典),第二个是更新函数。
  • 按 key 读取globals['my-param-key'] || false 把读取到的值规整为布尔值。这里体现了 Globals 的扁平字典形态:插件一般只关心自己命名空间下的 key(官方惯例是用前缀命名空间,例如 my-param-key,避免与工具栏等既有全局 key 冲突)。
  • 反向写入:按钮 onClick 调用 updateGlobals({ ['my-param-key']: !isActive }) 完成状态翻转,传入的是一个只包含自己 key 的局部对象。preview 端会对该对象执行合并更新(详见下文源码分析),因此不会清掉其他 key。
  • 驱动 UI 联动<AddonPanel active={isActive}> 的显隐完全由 globals 推导而来,按钮文案也随状态切换。点击后,状态更新会广播回管理器,触发使用 useGlobals 的组件重新渲染,形成“读取 → 更新 → 界面联动”的闭环。

值得一提的是,useGlobals 的真实返回值比示例中解构的更多(下文会展开),示例只取前两个就已经足够覆盖绝大多数用法,这也说明该 Hook 的设计对调用方足够宽容。

返回值拆解:globals / updateGlobals 之外还有什么

顺着解构的写法查看 manager 侧的真实实现,位于 root.tsx

export function useGlobals(): [
  globals: Globals,
  updateGlobals: (newGlobals: Globals) => void,
  storyGlobals: Globals,
  userGlobals: Globals,
] {
  const api = useStorybookApi();
  return [api.getGlobals(), api.updateGlobals, api.getStoryGlobals(), api.getUserGlobals()];
}

也就是说,useGlobals 实际返回一个四元组:

顺序 返回值 语义 底层实现
1 globals 当前生效的全局状态,即“用户设置的 globals 与 story 级 globals 叠加”后的结果 api.getGlobals(),读取 store 中的 globals 状态
2 updateGlobals(newGlobals) 更新全局状态并触发 preview 重新渲染 api.updateGlobals,通过事件通道发送给 preview
3 storyGlobals 当前 story 自己声明的 globals 覆盖值 api.getStoryGlobals()
4 userGlobals 用户/工具栏设置的 globals(可被 story 覆盖) api.getUserGlobals()

关于这三个 globals 变体之间的差异,globals.tsSubState 给出了清晰的分层:state 中同时维护 globalsuserGlobalsstoryGlobals 三份数据,其中 getGlobals 的 JSDoc 注释明确指出:“Returns the current globals, which is the user globals overlaid with the story globals”。因此日常插件只需消费第一项 globals 即可获得“真正对当前 story 生效”的状态;只有当你需要区分“这个值是用户点的还是某个 story 声明覆盖的”时,才需要用到第 3、4 项。

此外源码中还有一个值得留意的细节:预览端存在另一个同名但不同环境的 Hook —— hooks.ts 中的 useGlobals,它面向 preview 上下文(例如在 story 渲染环境内运行的代码),返回 [globals, updateGlobals],且更新动作通过 channel.emit(UPDATE_GLOBALS, ...) 发出。本文及示例使用的都是 manager 侧storybook/manager-api 版本,在编写插件 UI 组件时请确认导入路径正确。

底层原理:从按钮点击到预览 iframe 重渲染的事件闭环

updateGlobals 绝不是简单改一个本地变量,它是一条跨 iframe 的消息链。结合源码可以完整还原整个事件闭环。

第一步:manager 向 preview 发送 UPDATE_GLOBALS

manager 侧的 updateGlobals 实现位于 globals.ts

updateGlobals(newGlobals) {
  // Only emit the message to the local ref
  provider.channel?.emit(UPDATE_GLOBALS, {
    globals: newGlobals,
    options: {
      target: 'storybook-preview-iframe',
    },
  });
}

它并不直接修改任何本地状态,而是把新值打包进 UPDATE_GLOBALS 事件,并通过 target: 'storybook-preview-iframe' 指定投递目标为预览 iframe。manager 与 preview 运行在不同的 iframe/进程中,这条通道正是两者通信的桥梁。这一点在单元测试 globals.test.ts 中也有印证:测试断言调用 api.updateGlobals({ a: 'b' }) 后会在 channel 上发出 UPDATE_GLOBALS

第二步:preview 接收并合并,随后广播 GLOBALS_UPDATED

预览端在 Preview.tsx 中订阅了该事件:

setupListeners() {
  this.channel.on(STORY_INDEX_INVALIDATED, this.onStoryIndexChanged.bind(this));
  this.channel.on(UPDATE_GLOBALS, this.onUpdateGlobals.bind(this));
  // ...
}

真正的处理逻辑在 onUpdateGlobalsPreview.tsx)中:

  1. this.storyStoreValue.userGlobals.update(updatedGlobals) —— 把传入的局部 globals 合并进 userGlobals,因此前文强调“按 key 局部更新不会丢失其他 key”。
  2. 若当前存在选中的 story,则基于 story 上下文重新计算 { initialGlobals, userGlobals, storyGlobals, globals } 四份值并 channel.emit(GLOBALS_UPDATED, ...) 广播回 manager;若没有选中 story(例如处于 docs 模式),则用空的 storyGlobals 构造同样的回执。
  3. await Promise.all(this.storyRenders.map((r) => r.rerender())) —— 让所有正在渲染的 story 全部重渲染,使新 globals 真正作用于组件。

第三步:manager 收到广播并落库,触发 Hook 消费方重渲染

manager 侧 globals.tsGLOBALS_UPDATED 事件做了监听:它用 dequal 深度比较新旧值,只有发生变化时才把 globals / userGlobals / storyGlobals 写入 store。此外 preview 在初始化阶段还会通过 SET_GLOBALS(见 Preview.tsx)把初始 globals 与 globalTypes 推送给 manager,manager 则在 globals.ts 中落库并反向校准用户值。preview 端还记录了因收到 UPDATE_GLOBALS 而最终发出 GLOBALS_UPDATED 的行为测试(见 PreviewWeb.test.ts)。

把三步连起来看,一次 updateGlobals 调用对应的完整调用链是:

插件面板 onClick
  → manager: updateGlobals(newGlobals)
      → channel.emit(UPDATE_GLOBALS, { globals, options: { target: 'storybook-preview-iframe' } })
  → preview: onUpdateGlobals
      → storyStoreValue.userGlobals.update(updatedGlobals)      // 合并更新
      → 依据当前 story 计算 globals = userGlobals + storyGlobals
      → channel.emit(GLOBALS_UPDATED, { initialGlobals, userGlobals, storyGlobals, globals })
      → 所有 story 触发 rerender()
  → manager: GLOBALS_UPDATED 监听器
      → 深比较后写入 store 的 globals / userGlobals / storyGlobals
      → 所有 useGlobals() 组件读取新值并重渲染

这就是为什么点击一次按钮,preview 里的组件与 manager 里的插件 UI 会“同时”变化——它们都由同一份全局状态驱动。

高频重渲染风险与官方性能建议

理解了上述广播机制后,一个性能隐患也随之浮现:GLOBALS_UPDATED 每次都会把 state 变更广播给 manager 端所有订阅方,凡是调用 useGlobals 的组件都会重新渲染。官方在 addons-api.mdxuseGlobals 小节中给出了明确建议:

We also recommend optimizing your addon to rely on React.memo, or the following hooks; useMemo, useCallback to prevent a high volume of re-render cycles.

落实到工程实践中,通常建议:

  • 组件级:用 React.memo 包裹面板/工具栏组件,仅当 props 或自身读取的 globals 变化时才重渲染;
  • 计算级:对由 globals 派生的中间值使用 useMemo(例如基于 globals['my-param-key'] 计算出的显隐布尔值);
  • 回调级:对传给子组件的事件处理器使用 useCallback,避免每次渲染都生成新函数引用、拖累 React.memo 的浅比较优化;
  • 读取面最小化:与示例一致,尽量只读取自己命名空间下的 key(globals['my-param-key'])用于 UI 判断,而不是把整个 globals 对象散播给大量子组件,从而把重渲染影响范围控制在必要区域。

对于需要持久化更复杂、跨多种 UI 形态(如同时存在工具栏与面板)状态的需求,官方文档建议改用 useAddonState(同样见 addons-api.mdx),因为它是 addon 自己的状态通道,广播面比 Globals 小得多。

接入实战:把它注册成真正的 Storybook Addon

上文示例中的 Panel 只是一个组件,要让它在 Storybook UI 中真正出现,还需要经过 addons.registeraddons.add 两步注册(注册机制的说明位于 addons-api.mdxaddons.add() / addons.register() 小节)。在 my-addon/manager.js|ts(即插件的 manager 入口文件)中大致编排如下:

import { addons, types } from 'storybook/manager-api';
import { Panel } from './Panel'; // 包含上述 useGlobals 的组件

addons.register('my-addon', () => {
  addons.add('my-addon/panel', {
    type: types.PANEL,
    title: 'My addon panel',
    render: Panel,
  });
});

几个可以结合示例核对的要点:

  • render: Panel 的渲染函数中,Storybook 默认会以 { active } 等参数调用它;示例中的 Panel 组件并未解构 props,而是把显隐完全交给 globals['my-param-key'] 控制,并在 updateGlobals 中翻转该值——这是一种“用全局状态驱动插件自身 UI”的典型模式。你也可以在 storybook-addon-panel-example.md 等官方片段中对照不同面板写法的差异。
  • manager 入口文件需要在插件预设的 managerEntries 中被加载(参见 writing-addons.mdx 中关于 addon 结构的说明);而 useGlobals 只能出现在 manager 侧渲染的组件中,preview 侧请改用前文提到的 preview 版 Hook 或装饰器方案。
  • Globals 的“来源”即 globalTypes/globals 的项目级声明,统一在 .storybook/preview 中配置;两者如何定义、工具栏如何与之联动,可查阅 toolbars-and-globals.mdx。若需要同时“消费并更新某个 GlobalType”,官方片段 addon-consume-and-update-globaltype.mdaddon-consume-globaltype.md 提供了配套的读写参考。

小结

useGlobals 是连接插件 UI 与 Storybook 全局状态的“双向阀门”:通过 globals 读取当前生效值,通过 updateGlobals 发出局部更新,并由 manager → preview → manager 的事件闭环保证两端一致。实际使用中,请记住三点:导入自 storybook/manager-api、只按自己的 key 读写以避免状态互相污染、用 React.memo/useMemo/useCallback 抵御高频重渲染。沿着本文给出的源码路径(root.tsxglobals.tsPreview.tsx),你就能在调试插件时快速定位状态在两端传递中的每一个环节。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

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