Read OSS

The Manager UI: Modular State Management and Addon Integration

Advanced

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:

  1. Creates the browser channel with PostMessage + WebSocket transports
  2. Registers the channel with the addons singleton
  3. Emits CHANNEL_CREATED to 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() and useStorybookState() hooks from storybook/manager-api. These hooks consume the ManagerContext and 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 IndexHash mapping 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.