Skip to content

fix(stories): Fix HMR for .stories.tsx and .mdx files in Scraps#109630

Open
scttcper wants to merge 2 commits intomasterfrom
scttcper/fix-stories-hmr
Open

fix(stories): Fix HMR for .stories.tsx and .mdx files in Scraps#109630
scttcper wants to merge 2 commits intomasterfrom
scttcper/fix-stories-hmr

Conversation

@scttcper
Copy link
Member

Two separate problems were causing hot reload to break in the Scraps storybook.

Circular dependency crash

storybook.tsx was self-importing via the barrel (import * as Storybook from './') and importing makeStorybookDocumentTitle from storyExports.tsx, which itself imports the barrel. This cycle (barrel → storybook → barrel, storybook → storyExports → barrel → storybook) caused rspack's HMR to surface a crash on reload:

TypeError: Cannot read properties of undefined (reading 'ThemeSwitcher')

Fixed by replacing barrel imports in storybook.tsx and apiReference.tsx with direct imports from the modules they actually need. The view/ layer files (storyExports.tsx, storyHeader.tsx) are not part of any cycle because the barrel doesn't re-export from view/.

HMR only working once

The context references were const, so after the first HMR cycle the captured module reference became stale. The new context module had no accept handler registered, causing all subsequent saves to be silently dropped:

HMR Ignored an update to unaccepted module ./app/components/core/checkbox/checkbox.mdx

or in some cases triggering "unexpected require from disposed module".

Fixed by switching from require.context to import.meta.webpackContext (rspack's recommended ESM API) with let references. On each HMR update the onUpdate callback re-captures fresh context references and re-registers accept handlers against the new context IDs — otherwise after the first replacement the old handler is gone and HMR goes dead.

A useSyncExternalStore-based version counter threads the HMR signal into React Query, so the active story refetches cleanly after each save rather than requiring a manual page refresh.

Two separate problems were causing stories hot reload to break:

1. Circular dependency crash: `storybook.tsx` was self-importing via the
   barrel (`import * as Storybook from './'`) and also importing
   `makeStorybookDocumentTitle` from `storyExports.tsx`, which in turn
   imports the barrel. This created a cycle through the module graph that
   rspack's HMR couldn't resolve cleanly, surfacing as:
     TypeError: Cannot read properties of undefined (reading 'ThemeSwitcher')
   Fix: break both cycles by replacing the barrel import in `storybook.tsx`
   and `apiReference.tsx` with direct imports.

2. HMR only working once: `require.context` references were `const`, so
   after the first HMR cycle the captured reference was stale — the new
   context module had no accept handler, causing subsequent saves to show
   "Ignored an update to unaccepted module" or "unexpected require from
   disposed module". Fix: switch to `import.meta.webpackContext` with `let`
   references, re-capture them on each HMR update, and re-register accept
   handlers against the new context IDs. A `useSyncExternalStore`-based
   version counter threads the HMR signal into React Query so the active
   story refetches after each save.

Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions github-actions bot added the Scope: Frontend Automatically applied to PRs that change frontend components label Feb 28, 2026
@scttcper scttcper marked this pull request as ready for review February 28, 2026 00:43
@scttcper scttcper requested a review from a team as a code owner February 28, 2026 00:43
Comment on lines +31 to +41
});
mdxContext = import.meta.webpackContext('sentry', {
recursive: true,
regExp: /\.mdx$/,
mode: 'lazy',
});
import.meta.webpackHot!.accept(context.id as string, onUpdate);
import.meta.webpackHot!.accept(mdxContext.id as string, onUpdate);
_storiesHmrVersion++;
_storiesHmrListeners.forEach(l => l());
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The useStoryBookFiles hook uses useMemo with an empty dependency array, causing it to return a stale list of stories after an HMR update.
Severity: MEDIUM

Suggested Fix

Add the hmrVersion variable to the dependency array of the useMemo call within the useStoryBookFiles hook. This will ensure the list of story files is re-calculated whenever an HMR update occurs, similar to the pattern used in useStoriesLoader.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: static/app/stories/view/useStoriesLoader.tsx#L24-L41

Potential issue: The `useStoryBookFiles` hook memoizes its result using `useMemo` with
an empty dependency array. During a Hot Module Replacement (HMR) update, the
module-level variables `context` and `mdxContext` are reassigned with new story data.
However, since the `useMemo` has no dependencies, it doesn't re-evaluate and continues
to return the cached, stale list of story files from before the HMR update. As a result,
any new or modified stories will not appear in the story list sidebar until a full page
refresh is performed, undermining the HMR functionality.

Did we get this right? 👍 / 👎 to inform future reviews.

Copy link
Member Author

@scttcper scttcper Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

conflates two different scenarios: file modification (what HMR handles) vs. file addition/deletion (what requires a full rebuild).

The only scenario where useMemo([]) would matter is "I added a new story file and want it to appear without refreshing" — but that's not an HMR scenario,
it's a full rebuild. After the rebuild the component remounts anyway and the memo re-runs.

@scttcper scttcper requested a review from a team February 28, 2026 00:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Scope: Frontend Automatically applied to PRs that change frontend components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant