The Manager UI: Modular State Management and Addon Integration
Prerequisites
- ›Article 1: Architecture Overview
- ›Article 3: Channels and Events
- ›React component patterns (Context, class components)
- ›Understanding of state management patterns
The Manager UI: Modular State Management and Addon Integration
The Manager is the Storybook application you actually interact with — the sidebar, toolbar, addon panels, and settings pages. It's a full React application that manages complex state: the story tree, layout configuration, keyboard shortcuts, active globals, and the state of every registered addon. What makes its architecture distinctive is a modular state system where 14 independent modules each contribute a slice of state and API, composed via TypeScript intersection types into a single unified interface.
In Part 4, we explored the Preview's rendering pipeline. Now let's look at the other side of the PostMessage boundary.
Manager Bootstrap
The boot sequence starts in runtime.tsx, which is compiled as a "globalized runtime" entry (as we saw in Part 1):
code/core/src/manager/runtime.tsx#L32-L89
sequenceDiagram
participant HTML as index.html
participant RT as runtime.tsx
participant RP as ReactProvider
participant Channel as Channel
participant UI as renderStorybookUI
HTML->>RT: Script loaded
RT->>RT: Register toolbar addon
RT->>RP: new ReactProvider()
RP->>Channel: createBrowserChannel({ page: 'manager' })
RP->>Channel: addons.setChannel(channel)
RP->>Channel: emit(CHANNEL_CREATED)
RT->>UI: renderStorybookUI(rootEl, provider)
The ReactProvider class (line 32) does three things in its constructor:
- Creates the browser channel with PostMessage + WebSocket transports
- Registers the channel with the addons singleton
- Emits
CHANNEL_CREATEDto signal readiness
It also sets up a CHANNEL_WS_DISCONNECT handler that shows a notification when the WebSocket connection to the dev server is lost — a common occurrence when the server crashes or times out.
The actual rendering is deferred with setTimeout(() => renderStorybookUI(...), 0) (line 86). This ensures that global script tags in the HTML (which set CHANNEL_OPTIONS, CONFIG_TYPE, etc.) have been evaluated before React attempts to use them.
The Component Tree
The Manager's React tree has a clear layered structure:
code/core/src/manager/index.tsx#L32-L99
flowchart TD
Root["Root"] --> Helmet["HelmetProvider"]
Helmet --> Location["LocationProvider"]
Location --> Main["Main"]
Main --> LocationConsumer["Location (consumer)"]
LocationConsumer --> MP["ManagerProvider"]
MP --> Theme["ThemeProvider"]
Theme --> LP["LayoutProvider"]
LP --> App["App"]
App --> Layout["Layout"]
Layout --> Sidebar
Layout --> Preview["Preview iframe"]
Layout --> Panel["Addon Panel"]
Each wrapper has a specific role:
- HelmetProvider: Manages document
<head>elements (title, meta tags) - LocationProvider: URL routing and history management
- ManagerProvider: The state management core (14 modules, unified state)
- ThemeProvider: Emotion-based theming via
storybook/theming - LayoutProvider: Layout measurement and resize state
The renderStorybookUI function (line 92) validates that the provider extends the base Provider class, creates a React root, and mounts the tree. This validation prevents a common error when third-party code passes an incorrect provider object.
ManagerProvider and the Module System
The heart of the Manager's state management is ManagerProvider, a React class component that initializes 14 modules and composes their state and API:
code/core/src/manager-api/root.tsx#L82-L110
The State and API types are composed using TypeScript intersection types:
export type State = layout.SubState &
stories.SubState &
refs.SubState &
notifications.SubState &
version.SubState &
url.SubState &
shortcuts.SubState &
settings.SubState &
globals.SubState &
whatsnew.SubState &
RouterData &
API_OptionsData &
Other;
export type API = addons.SubAPI &
channel.SubAPI &
provider.SubAPI &
stories.SubAPI &
refs.SubAPI &
globals.SubAPI &
layout.SubAPI &
notifications.SubAPI &
shortcuts.SubAPI &
settings.SubAPI &
version.SubAPI &
url.SubAPI &
whatsnew.SubAPI &
openInEditor.SubAPI &
Other;
classDiagram
class ManagerProvider {
+api: API
+modules: ModuleFn[]
+state: State
+initModules()
+render()
}
class ModuleFn {
<<interface>>
+init(args): ModuleResult
}
class ModuleResult {
+state: SubState
+api: SubAPI
+init(): void
}
ManagerProvider --> "14" ModuleFn : initializes
ManagerProvider --> "1" State : composes
ManagerProvider --> "1" API : composes
In the constructor (lines 130–197), each module is initialized with a shared set of dependencies:
code/core/src/manager-api/root.tsx#L130-L197
this.modules = [
provider, channel, addons, layout,
notifications, settings, shortcuts, stories,
refs, globals, url, version, whatsnew, openInEditor,
].map((m) =>
m.init({ ...routeData, ...optionsData, ...apiData, state: this.state, fullAPI: this.api })
);
Each module's init function receives the full API reference (initially an empty object that gets populated as modules are initialized). This allows modules to call each other's APIs. The circular reference is resolved in two phases: first, all modules return their state slices and API methods; then, initModules() is called as a React effect, allowing modules to perform initialization that depends on other modules being ready.
Tip: When building a Storybook addon that needs access to the Manager's state, use the
useStorybookApi()anduseStorybookState()hooks fromstorybook/manager-api. These hooks consume theManagerContextand give you the full composed API and state.
Key Modules Deep Dive
Stories Module
The stories module is the largest and most complex. It manages:
- The story tree (an
IndexHashmapping IDs to entries) - Story selection and navigation
- Story filtering (by tags, status, search)
- Args state synchronization with the Preview
code/core/src/manager-api/modules/stories.ts#L1-L80
It listens to channel events like SET_INDEX (when the story index changes), STORY_PREPARED (when a story's full metadata is available), and STORY_ARGS_UPDATED (when args change in the Preview). It also handles STORY_INDEX_INVALIDATED by re-fetching index.json from the server.
Layout Module
The layout module controls the Manager's visual structure: sidebar width, panel position (bottom/right), panel visibility, full-screen mode, and the active addon tab. Its state is persisted to localStorage so your layout preferences survive page reloads.
Shortcuts Module
The shortcuts module manages keyboard bindings (toggle sidebar, toggle panel, go to next/previous story, etc.) and handles keydown events from both the Manager frame and the Preview iframe (forwarded via PREVIEW_KEYDOWN).
Addon Registration and Slots
Addons integrate with the Manager through a registration API:
// Typical addon manager entry
import { addons, types } from 'storybook/manager-api';
addons.register('my-addon', (api) => {
addons.add('my-addon/panel', {
type: types.PANEL,
title: 'My Panel',
render: ({ active }) => active ? <MyPanel /> : null,
});
});
The Manager builder (esbuild-based) bundles all addon manager entries:
code/core/src/builder-manager/index.ts#L36-L120
flowchart TD
Presets["presets.apply('managerEntries')"] --> Entries["Collect all manager entries"]
ConfigDir["configDir/manager.ts"] --> Entries
Entries --> Wrap["wrapManagerEntries()"]
Wrap --> ESBuild["esbuild bundle (IIFE format)"]
ESBuild --> Output["sb-addons/*.js"]
subgraph "Available Slot Types"
PANEL["types.PANEL — Addon panels"]
TOOL["types.TOOL — Toolbar tools"]
TAB["types.TAB — Canvas tabs"]
PAGE["types.experimental_PAGE — Full pages"]
end
Each entry is wrapped in a try/catch (lines 108–111) so a failing addon doesn't crash the entire Manager. The esbuild config uses globalExternals to ensure addons share the same React, storybook/manager-api, and other globals as the Manager itself rather than bundling their own copies.
The available slot types define where an addon can inject UI:
- PANEL: Appears in the addon panel below or beside the preview
- TOOL: Appears in the toolbar
- TAB: Appears as a tab alongside the Canvas
- experimental_PAGE: A full-page route (used by Settings)
The Ref System: Storybook Composition
The refs module enables Storybook Composition — loading stories from multiple Storybook instances into a single UI:
code/core/src/manager-api/modules/refs.ts#L1-L60
flowchart TB
Main["Main Storybook"] --> Sidebar
Sidebar --> LocalStories["Local Stories (IndexHash)"]
Sidebar --> Ref1["Ref: Design System Storybook"]
Sidebar --> Ref2["Ref: Shared Components Storybook"]
Ref1 -->|"fetch /index.json"| Remote1["Remote index.json"]
Ref2 -->|"fetch /index.json"| Remote2["Remote index.json"]
Ref1 -->|"iframe src"| Preview1["Remote Preview iframe"]
Ref2 -->|"iframe src"| Preview2["Remote Preview iframe"]
When refs are configured in main.ts, the Manager fetches each remote Storybook's index.json, transforms the story index, and merges it into the sidebar alongside local stories. When a user selects a remote story, the Preview iframe loads from the remote URL instead of the local server.
The SubAPI interface for refs provides methods to findRef, setRef, updateRef, getRefs, and checkRef. The state tracks each ref's loading status, stories, and version information.
Tip: Storybook Composition is powerful for design systems. Publish your design system's Storybook as a static site, then reference it in consuming applications' Storybook configs. Users see all stories in one sidebar without duplicating component code.
What's Next
We've now covered all three runtime environments in detail: the Server (Part 1–2), the Preview (Part 4), and the Manager (this article). In the final article, we'll zoom out to the build infrastructure that makes all of this possible: the Nx-orchestrated monorepo pipeline, the story index generator, change detection, sandbox testing, and the structured error system.