Skip to content

Maintenance: extract shared Next.js preview setup to eliminate nextjs/nextjs-vite preview.tsx duplication#34475

Closed
mixelburg wants to merge 1 commit into
storybookjs:nextfrom
mixelburg:fix/nextjs-preview-dedup
Closed

Maintenance: extract shared Next.js preview setup to eliminate nextjs/nextjs-vite preview.tsx duplication#34475
mixelburg wants to merge 1 commit into
storybookjs:nextfrom
mixelburg:fix/nextjs-preview-dedup

Conversation

@mixelburg
Copy link
Copy Markdown

@mixelburg mixelburg commented Apr 6, 2026

Closes #34465

What I did

code/frameworks/nextjs/src/preview.tsx and code/frameworks/nextjs-vite/src/preview.tsx contained ~55 lines of near-identical code: addNextHeadCount(), isAsyncClientComponentError(), the global console.error patch, the error event listener override, and the entire parameters export.

Created code/frameworks/nextjs/src/preview-shared.ts with:

  • addNextHeadCount()
  • isAsyncClientComponentError()
  • setupNextErrorPatching() — wraps the console.error patch + error event listener
  • parameters export

Both preview.tsx files now import these from the shared module and call setupNextErrorPatching() at module load time, preserving the original execution order.

Files changed:

  • New code/frameworks/nextjs/src/preview-shared.ts — shared utilities
  • code/frameworks/nextjs/src/preview.tsx — imports from shared module
  • code/frameworks/nextjs-vite/src/preview.tsx — imports from @storybook/nextjs/preview-shared
  • Added ./preview-shared export entry to @storybook/nextjs package.json
  • Added @storybook/nextjs: workspace:* to @storybook/nextjs-vite dependencies

The decorators and loaders sections remain intentionally different between the two files (different type signatures for vite vs webpack).

Note: This PR adds @storybook/nextjs as a dependency of @storybook/nextjs-vite — the same dependency already added in #34474 for the portable-stories dedup. If that PR merges first, the package.json change here will be a no-op.

Checklist for Contributors

Testing

  • stories
  • unit tests
  • integration tests
  • end-to-end tests

Manual testing

Pure refactor — no behavior changes. Both files call setupNextErrorPatching() once at module load time, same as the inline code before.

Summary by CodeRabbit

  • Refactor
    • Consolidated Next.js error handling utilities into a shared module for improved consistency across framework integrations and easier maintenance.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 6, 2026

📝 Walkthrough

Walkthrough

Refactored Next.js Storybook integration by extracting error handling utilities from nextjs/src/preview.tsx into a new shared module (nextjs/src/preview-shared.ts), which is now consumed by both the nextjs and nextjs-vite frameworks, centralizing Next.js-specific error suppression logic.

Changes

Cohort / File(s) Summary
Shared Preview Module
code/frameworks/nextjs/src/preview-shared.ts, code/frameworks/nextjs/package.json
New file introducing setupNextErrorPatching(), addNextHeadCount(), isAsyncClientComponentError() utilities and shared parameters configuration. Added ./preview-shared export subpath to package exports.
Preview Refactoring
code/frameworks/nextjs/src/preview.tsx, code/frameworks/nextjs-vite/src/preview.tsx
Both files refactored to import error patching from preview-shared and replace inline error suppression logic with setupNextErrorPatching() call; parameters changed from inline definitions to re-exports.
Dependencies
code/frameworks/nextjs-vite/package.json
Added @storybook/nextjs as workspace dependency to enable consumption of preview-shared module.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
code/frameworks/nextjs/src/preview-shared.ts (1)

3-39: Make global patch setup idempotent.

setupNextErrorPatching() currently re-patches console.error, re-adds the global error listener, and re-appends next-head-count on every invocation. A one-time guard avoids stacked wrappers/listeners during re-import/HMR.

Proposed hardening
 import { isNextRouterError } from 'next/dist/client/components/is-next-router-error.js';
 
+let nextErrorPatchingInstalled = false;
+
 export function addNextHeadCount() {
+  if (document.head.querySelector('meta[name="next-head-count"]')) {
+    return;
+  }
   const meta = document.createElement('meta');
   meta.name = 'next-head-count';
   meta.content = '0';
   document.head.appendChild(meta);
 }
@@
 export function setupNextErrorPatching() {
+  if (nextErrorPatchingInstalled) {
+    return;
+  }
+  nextErrorPatchingInstalled = true;
+
   addNextHeadCount();
@@
   globalThis.addEventListener('error', (ev: WindowEventMap['error']): void => {
     if (isNextRouterError(ev.error) || isAsyncClientComponentError(ev.error)) {
       ev.preventDefault();
       return;
     }
   });
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/frameworks/nextjs/src/preview-shared.ts` around lines 3 - 39, The
setupNextErrorPatching function should be made idempotent: add a one-time guard
(e.g., a module-level flag or a symbol on globalThis such as
globalThis.__nextErrorPatchingDone) checked at the top of setupNextErrorPatching
to return early if already applied; modify addNextHeadCount to no-op if a
meta[name="next-head-count"] already exists; only wrap globalThis.console.error
if it hasn't been wrapped yet (detect via the guard or by checking a marker on
the function) and only add the globalThis.addEventListener('error', ...)
listener once (store the listener reference or rely on the same guard to avoid
re-adding). Ensure the guard is set after successfully applying the patches so
repeated calls to setupNextErrorPatching or HMR re-imports do nothing.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@code/frameworks/nextjs/package.json`:
- Around line 74-78: The package export for "./preview-shared" is declared in
package.json but no build entry is configured to produce dist/preview-shared.*;
update build-config.ts to add an export entry that builds src/preview-shared.ts
by adding exportEntries: ['./preview-shared'] (or include
'./src/preview-shared.ts' as a build entry) so the bundler emits
dist/preview-shared.js and dist/preview-shared.d.ts; locate the configuration in
build-config.ts and add the new entry alongside the other Storybook/Next.js
entries to ensure `@storybook/nextjs/preview-shared` imports resolve.

---

Nitpick comments:
In `@code/frameworks/nextjs/src/preview-shared.ts`:
- Around line 3-39: The setupNextErrorPatching function should be made
idempotent: add a one-time guard (e.g., a module-level flag or a symbol on
globalThis such as globalThis.__nextErrorPatchingDone) checked at the top of
setupNextErrorPatching to return early if already applied; modify
addNextHeadCount to no-op if a meta[name="next-head-count"] already exists; only
wrap globalThis.console.error if it hasn't been wrapped yet (detect via the
guard or by checking a marker on the function) and only add the
globalThis.addEventListener('error', ...) listener once (store the listener
reference or rely on the same guard to avoid re-adding). Ensure the guard is set
after successfully applying the patches so repeated calls to
setupNextErrorPatching or HMR re-imports do nothing.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 411166f4-bc65-4449-a7f8-c6a8e1024076

📥 Commits

Reviewing files that changed from the base of the PR and between 75fa10a and ece67c7.

📒 Files selected for processing (5)
  • code/frameworks/nextjs-vite/package.json
  • code/frameworks/nextjs-vite/src/preview.tsx
  • code/frameworks/nextjs/package.json
  • code/frameworks/nextjs/src/preview-shared.ts
  • code/frameworks/nextjs/src/preview.tsx

Comment on lines +74 to +78
"./preview-shared": {
"types": "./dist/preview-shared.d.ts",
"code": "./src/preview-shared.ts",
"default": "./dist/preview-shared.js"
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

./preview-shared export targets are currently not build-produced.

Lines 74-78 expose ./dist/preview-shared.*, but code/frameworks/nextjs/build-config.ts does not define an entry for ./src/preview-shared.ts with exportEntries: ['./preview-shared']. This can ship a broken export and break imports such as @storybook/nextjs/preview-shared.

Proposed fix (add missing build entry)
diff --git a/code/frameworks/nextjs/build-config.ts b/code/frameworks/nextjs/build-config.ts
@@
       {
         exportEntries: ['./preview'],
         entryPoint: './src/preview.tsx',
       },
+      {
+        exportEntries: ['./preview-shared'],
+        entryPoint: './src/preview-shared.ts',
+      },
       {
         exportEntries: ['./config/preview'],
         entryPoint: './src/config/preview.ts',
         dts: false,
       },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"./preview-shared": {
"types": "./dist/preview-shared.d.ts",
"code": "./src/preview-shared.ts",
"default": "./dist/preview-shared.js"
},
{
exportEntries: ['./preview'],
entryPoint: './src/preview.tsx',
},
{
exportEntries: ['./preview-shared'],
entryPoint: './src/preview-shared.ts',
},
{
exportEntries: ['./config/preview'],
entryPoint: './src/config/preview.ts',
dts: false,
},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/frameworks/nextjs/package.json` around lines 74 - 78, The package export
for "./preview-shared" is declared in package.json but no build entry is
configured to produce dist/preview-shared.*; update build-config.ts to add an
export entry that builds src/preview-shared.ts by adding exportEntries:
['./preview-shared'] (or include './src/preview-shared.ts' as a build entry) so
the bundler emits dist/preview-shared.js and dist/preview-shared.d.ts; locate
the configuration in build-config.ts and add the new entry alongside the other
Storybook/Next.js entries to ensure `@storybook/nextjs/preview-shared` imports
resolve.

@valentinpalkovic
Copy link
Copy Markdown
Contributor

Closing due to #34453 (comment)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Duplicate Code: Identical preview.tsx Error Handling Logic in nextjs and nextjs-vite Frameworks

2 participants