From Story File to Isolated Preview

A story becomes useful only when Storybook can find it, give it a stable identity, load it at the right time, and render it without losing track of its state. This chapter follows that path through the story index, the manager, the preview iframe, and the rendering lifecycle. The practical goal is diagnostic: when a story is missing, slow, stale, or failing, readers should be able to ask which transition failed instead of treating Storybook as one opaque box.

The index makes a large story collection navigable

The process begins in the project configuration. Storybook’s main configuration specifies where story files live, usually with glob patterns that match files such as Button.stories.tsx. The glob is a selection rule, not an instruction to execute every matching module immediately. It tells Storybook which files are candidates for indexing.

On the server side, the story index generator finds the matching files and chooses an indexer that understands each file’s format. The indexer extracts the information needed to describe the entries: a title, a story name, an identity, an import path, a component path when available, and tags. When automatic documentation is enabled, the same process may add a documentation entry associated with the story file. A separate MDX document can also become an entry and can declare which story files it uses.

This distinction between description and execution is the reason for the index. The manager needs to build a sidebar, resolve a URL, sort entries, apply filters, and choose an initial selection. It does not need to execute every component merely to know that a story exists. The server can perform filesystem and configuration work once, produce a compact representation, and let the manager operate on that representation. A project with many stories still has a large collection, but navigation is no longer coupled to running every component at startup.

The index also gives each state an address. Storybook derives or reads titles and names, combines tags from the project and story scopes, and uses them to construct an identity. That identity links several surfaces: the sidebar entry, the URL, the module that will be imported, and later the reports associated with the story. For duplicate story entries, the generator treats the ambiguity as an indexing problem rather than silently choosing an arbitrary component. Documentation entries have separate de-duplication and override rules, so not every repeated docs entry has the same outcome.

Documentation adds a dependency wrinkle. An MDX page may refer to the full set of exports from a CSF story file. The generator therefore processes story files before documentation files and tracks which story entries a docs page depends on. If the story file changes, the dependent docs entry can be invalidated as well. This is not an incidental implementation preference: documentation is another view of the story artifact, so changes to the artifact can affect both the story and the page that presents it.

The development server exposes the index through index.json. It watches the configured story files and relevant configuration files. When a file is added, changed, or removed, the server invalidates the affected cached entry and emits an index-invalidation event. The manager can then fetch a new index and rebuild its view of the collection. A preview that has already selected a story receives the corresponding update and decides whether it must load, rerender, or remove something.

This makes several failure modes easier to name. If the glob is wrong, the story never enters the index. If the story format cannot be processed, indexing can fail before any component is rendered. If a docs page references a CSF file that is not matched or cannot be loaded, the docs entry cannot be resolved. If an ID collides, navigation cannot safely choose one entry. These are different from a component throwing during rendering, even though a user may first experience all of them as “the story is broken.”

The index is not a complete copy of a story. It can say which module to import and what entry to display, but it does not contain the component’s live DOM, its current args, or the effects of its play function. That information must cross the next boundary.

sequenceDiagram participant Files as Story files and config participant Server as Story index generator participant Manager as Manager UI participant Preview as Preview iframe participant Store as StoryStore participant Renderer as Component renderer Files->>Server: Match globs and extract metadata Server-->>Manager: Serve indexed stories and docs Manager->>Preview: Select story identity Preview->>Store: Load module through importFn Store->>Store: Process CSF and prepare context Store->>Renderer: Mount story with annotations Renderer-->>Preview: Render and run lifecycle/play Preview-->>Manager: Emit prepared, finished, or error state Files->>Server: Change story or preview config Server-->>Manager: Invalidate and refresh index Manager->>Preview: Rerender or remount selection

The sequence shows two kinds of work. The first pass is descriptive: files become index entries. The second is executable: a selected identity causes a module import and a render. A file change can re-enter the descriptive pass and then trigger a new executable pass. That loop is what keeps the sidebar and the iframe related without making them the same runtime.

Selection turns metadata into a live preview

The manager is the application around the story. Its React tree owns navigation, the sidebar, filters, layout, addon surfaces, and status information. The preview is the runtime that owns the component DOM inside the iframe. The manager selects an indexed identity; the preview resolves that identity and renders it. They communicate over a channel rather than sharing one in-memory component tree.

When the preview initializes, it first obtains project annotations and fetches the story index from the server. It creates a StoryStore with that index, the function that can import story modules, and the project configuration. The store can then map a story ID to an import path, load the relevant CSF module, and combine the story’s metadata with project and component annotations.

In the Vite path, the builder generates an import function whose entries use asynchronous dynamic imports. The function maps normalized story paths to importers. That choice delays execution of a story module until Storybook needs it and allows the bundler to split the modules into smaller chunks. The index can be available before the component code has run. The exact bundling result still depends on the build mode and builder configuration, so “loaded on demand” should not be confused with “never included in a production bundle.”

The StoryStore’s preparation step creates the renderable form of the selected story. It processes the CSF exports, combines project, component, and story annotations, and applies argument and argument-type enhancers. The prepared story contains the render function, decorators, loaders, lifecycle hooks, play function, parameters, tags, and renderer callback needed for a run. It is intentionally not the owner of changing args and globals. Those values are stored separately and supplied when a context is created.

That separation explains a familiar interaction. A user changes a value in Controls. The manager emits an args-update event containing the story identity and changed values. The preview updates the story’s args store and asks the relevant render to rerender. The story definition has not been rewritten; the same prepared story is invoked with a different live context. If the story’s play function needs a fresh mount, the preview can remount instead of simply calling the renderer again. The choice depends on how the story declared its mounting behavior.

Globals follow a similar path. A toolbar may change a theme, viewport, locale, or another project-level setting. The preview updates its global state, emits the resulting values, and rerenders the story renders that depend on them. A global setting is not just a manager decoration: it crosses into the preview context and can alter what the component receives or how an addon decorates it.

The preview’s selection logic also distinguishes story and documentation entries. A selected story is rendered into the story root. A docs entry is rendered into a docs root and may render several story examples on one page. The same story can therefore be prepared for a single canvas or embedded as one part of a documentation page. The index gives both entries an identity, but the render path depends on the entry type.

The manager and preview keep each other informed through explicit events. Selection events tell the preview which story to render. Preparation events send the manager the resolved parameters, initial args, argument types, and current args. Render and finish events tell the manager whether the run completed successfully. Error events distinguish a missing story, a preview configuration problem, an exception while rendering, or a failure during play. Args and globals events travel in the opposite direction when the manager changes live state.

This protocol also supports preloading. After a story is prepared, the manager can ask the preview to load neighboring entries before the user selects them. Preloading does not change the current selection; it reduces the wait for likely navigation. The preview has to target the correct iframe, especially when references or composed Storybooks are present. The source explicitly guards against sending a preload event to an iframe that has not yet been created, because a fallback to the active iframe could load the wrong story.

Rendering is a lifecycle, not a single mount call

Once the selected story is prepared, StoryRender moves through recognizable phases. It prepares the story, loads declared data, runs beforeEach setup, renders through the renderer, runs the play function when appropriate, waits for or pauses animations, emits completion, runs afterEach, and reports a final status. The lifecycle gives addons and the manager points at which they can observe or contribute work.

The initial render usually mounts the component and then runs play. A play function can query the story canvas, simulate user actions, and make assertions. If play uses the explicit mount flow, rendering can occur inside that function so data or setup is ready before the component appears. This is why the preview tracks whether mount was used: it changes the valid order of rendering and playing.

A successful render is therefore more than “the component function returned.” The renderer must mount the output into the right root, the declared loaders must finish, setup must not abort, play must not throw, and the final context must not contain unhandled errors or failed reports. If a story errors, the preview emits the relevant event and the manager can show the failure beside the story. The component’s visible exception and the system’s lifecycle status are connected but not identical; a story can render and later fail in play or an addon’s after-each check.

Changing stories while any of this is happening creates the hard part. An old render may still be loading data or running a play function when a new selection arrives. StoryRender uses an abort signal to tell asynchronous work that it should stop, and the store runs registered cleanup callbacks. The preview waits briefly for pending work to leave before removing the old render. When it is replacing the same story after a hot update, it can keep the existing DOM mounted until the new render commits, avoiding a visible collapse and preserving scroll position.

Cancellation is cooperative, not magical. Code inside a loader or play function can ignore the signal or hold resources outside Storybook’s control. If the old render remains pending after the bounded cleanup attempt, the implementation falls back to reloading the iframe to obtain a clean slate. That is a reliability boundary worth remembering: the isolated preview makes state easier to reproduce, but asynchronous user code can still create races and leaked state.

HMR uses the same lifecycle rather than bypassing it. A changed story file causes the server or builder to invalidate the relevant module and index data. The preview receives a stories-changed event, updates its import function or index, and renders the current selection again if one exists. A changed preview configuration can invalidate all story entries because project-level annotations affect every prepared story. A same-story update may keep the old screen visible during preparation, while a new story or view mode shows a preparing state.

The completed path is thus a sequence of boundaries:

  1. Configuration to index: a glob decides which files are candidates, and the server extracts selectable metadata.
  2. Index to module: a selected identity resolves to an import path and dynamic importer.
  3. Module to prepared story: CSF and layered annotations become a renderable, normalized story.
  4. Prepared story to live context: current args, globals, hooks, and reporting state are attached for this run.
  5. Context to preview DOM: the renderer mounts, play and lifecycle work execute, and events report progress.
  6. Active render to next render: args changes, HMR, or navigation trigger rerender/remount with cleanup and cancellation.

Each boundary has a different owner and a different failure surface. A missing sidebar entry points toward discovery or indexing. A listed story that fails only when selected points toward import, preparation, or rendering. A story that looks correct but fails after a click points toward its play function or post-render checks. A failure that appears only after hot reload may involve cancellation or stale module state. The benefit of knowing the lifecycle is not merely theoretical; it narrows where to look.

Storybook’s central idea now has an operational form. A story begins as a named description of a UI state, but the platform gives that description an indexed identity, a lazy loading path, a live context, a renderer lifecycle, and a feedback channel. The next step is to hold those parts together as one model: which values persist, which values change, and how one story can be adapted to the different platform layers around it.

References