fix(stories): Fix HMR for .stories.tsx and .mdx files in Scraps#109630
fix(stories): Fix HMR for .stories.tsx and .mdx files in Scraps#109630
Conversation
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>
| }); | ||
| 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()); | ||
| }; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
Two separate problems were causing hot reload to break in the Scraps storybook.
Circular dependency crash
storybook.tsxwas self-importing via the barrel (import * as Storybook from './') and importingmakeStorybookDocumentTitlefromstoryExports.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:Fixed by replacing barrel imports in
storybook.tsxandapiReference.tsxwith direct imports from the modules they actually need. Theview/layer files (storyExports.tsx,storyHeader.tsx) are not part of any cycle because the barrel doesn't re-export fromview/.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:or in some cases triggering "unexpected require from disposed module".
Fixed by switching from
require.contexttoimport.meta.webpackContext(rspack's recommended ESM API) withletreferences. On each HMR update theonUpdatecallback 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.