Thanks to visit codestin.com
Credit goes to github.com

Skip to content

fix: exclude shared singleton React from dev optimizeDeps#861

Merged
gioboa merged 2 commits into
module-federation:mainfrom
dmchoi77:fix/react-singleton-dev-optimize-deps
Jun 26, 2026
Merged

fix: exclude shared singleton React from dev optimizeDeps#861
gioboa merged 2 commits into
module-federation:mainfrom
dmchoi77:fix/react-singleton-dev-optimize-deps

Conversation

@dmchoi77

@dmchoi77 dmchoi77 commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Partially addresses #858

Summary

This prevents shared singleton React from being pre-bundled by Vite's dev optimizeDeps pipeline.

Previously, react was excluded from dev dependency optimization only when react-redux was installed. React-only or React-without-Redux Module Federation setups could still have react pushed into optimizeDeps.include, allowing Vite to pre-bundle a remote-local React copy under /node_modules/.vite/deps.

The fix changes the bypass condition to use the shared configuration itself: if react is configured as a shared singleton, it stays out of dev pre-bundling.

Problem

React singleton sharing depends on bare React imports going through the federation shared/loadShare path.

In dev mode, Vite can rewrite optimized dependencies to files such as:

/node_modules/.vite/deps/react.js

Once that happens, the remote can evaluate against its own optimized React instance while the host's react-dom renders with the host React dispatcher. That produces duplicate React symptoms such as:

Invalid hook call
Cannot read properties of null (reading 'useSyncExternalStore')

The previous guard was too narrow:

key === 'react' && hasPackageDependency('react-redux', root)

That only protected projects with react-redux installed. Shared singleton React should not depend on whether Redux is present.

Changes

  • Changed the dev optimizeDeps bypass condition for React to use shareItem.shareConfig.singleton === true.
  • Kept the existing Lit bypass behavior unchanged.
  • Added a regression test that verifies shared singleton React is excluded from dev optimizeDeps even when react-redux is not installed.

Tests

  • Added coverage in src/__tests__/index.test.ts for:
    • react-redux absent,
    • react configured as a shared singleton,
    • dev mode optimizeDeps excluding react,
    • dev mode optimizeDeps not including react.

Verification

pnpm exec vitest run src/__tests__/index.test.ts -t "excludes shared singleton react from dev optimizeDeps without react-redux"
pnpm exec vitest run src/__tests__/index.test.ts
pnpm exec tsc -p tsconfig.json --noEmit
pnpm exec vitest run --dir src
git diff --check -- src/index.ts src/__tests__/index.test.ts

Results:

  • Regression test failed before the implementation change and passed after it.
  • src/__tests__/index.test.ts passed: 56 tests.
  • TypeScript check passed.
  • Source tests passed: 34 files / 563 tests.
  • Diff whitespace check passed.

pnpm exec vitest run --dir src still prints the existing MaxListenersExceededWarning, but all tests pass.

Compatibility Notes

This change only affects dev-mode optimizeDeps handling for shared singleton react.

Non-singleton React remains on the existing optimizeDeps include path. User-provided optimizeDeps.include still takes precedence through the existing include check, preserving the current explicit override behavior.

Projects that need React singleton sharing should avoid explicitly adding react to optimizeDeps.include, because that intentionally opts back into Vite dev pre-bundling.

@pkg-pr-new

pkg-pr-new Bot commented Jun 26, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@module-federation/vite@861

commit: 503281b

@gioboa gioboa left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I see, that check was introduced for #792.
Can you check if this change is working in that case too?

@dmchoi77

Copy link
Copy Markdown
Contributor Author

I checked the #792 case as well.

I ran the existing #800 regression test for the react-redux path:

pnpm exec vitest run src/__tests__/index.test.ts -t "excludes shared react from dev optimizeDeps when react-redux is installed"

It passes with this change, so the #792 behavior is preserved.

@abhayzmp

Copy link
Copy Markdown

Tested #861 against the repro — it's necessary but not sufficient

Thanks for the quick PR @dmchoi77. I tested it directly against the minimal repro (https://github.com/abhayzmp/mf-vite-repro) and unfortunately the duplicate-React error still reproduces. I believe the bug has two independent causes and #861 only addresses the first.

How I tested

I applied #861's exact source change to the installed plugin in node_modules:

// src/index.ts (built: lib/index.js)
- const shouldBypassOptimizeDep = isLitShare(key) || key === "react" && hasPackageDependency("react-redux", root);
+ const shouldBypassOptimizeDep = isLitShare(key) || key === "react" && shareItem.shareConfig?.singleton === true;

Important detail: the host and remote resolve to two separate installed copies of the plugin, because they're on different Vite majors:

I patched both copies (verified the shouldBypassOptimizeDep line in each now uses the singleton gate), then restarted with forced re-optimization:

pnpm dev --force   # vite --force on both apps → "Forced re-optimization of dependencies"

Result

The remote still fails with the same error:

TypeError: Cannot read properties of null (reading 'useSyncExternalStore')
Invalid hook call. ... You might have more than one copy of React in the same app

The stack trace shows react + react-i18next resolving from the remote origin (http://localhost:5001) while react-dom resolves from the host (http://localhost:5173) — i.e. the remote is still ending up with its own React instance even though React is now correctly excluded from its dev pre-bundle.

Why I think there are two causes

Cause 1 — the react-redux gate (what #861 fixes). #800 only excluded React from dev optimizeDeps when react-redux was installed. #861 re-gates that on singleton, which is correct. ✅

Cause 2 — the 1.16.9 share-scope cache-key / singleton-aliasing rewrite (not addressed). After the 1.16.9 cluster, even with React correctly excluded from pre-bundling, the remote's loadShare no longer resolves onto the host's React singleton. The relevant PRs:

This matches the bisection: 1.16.6 is the last working version precisely because it predates this rewrite. The dev optimizeDeps exclude (whether automatic or set manually) stopped being sufficient from 1.16.9 onward.

Bisection (no react-redux in the stack)

@module-federation/vite Result
1.16.4 ✅ Works
1.16.5 Invalid hook call
1.16.6 ✅ Works
1.16.9 Cannot read properties of undefined (reading '0')
1.16.10 Cannot read properties of null (reading 'useSyncExternalStore')
1.16.10 + #861 ❌ Still useSyncExternalStore (tested as above)

Suggestion

#861 looks correct and worth merging on its own (the react-redux gate shouldn't have been there). But a complete fix for #858 also needs the share-scope singleton-aliasing path restored so a remote's loadShare resolves onto the host's React singleton in dev — likely a regression introduced by #828 / #837 / #843.

Happy to re-test any follow-up patch against the same repro.

@abhayzmp

Copy link
Copy Markdown

Now that the repo is public and accessible, please feel free to play around, clone and test your changes on it. thanks

@gioboa gioboa left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Tests With Different Frameworks

Image

@dmchoi77

Copy link
Copy Markdown
Contributor Author

I checked this further, and I agree: #861 is necessary but not sufficient, so I’ll keep this PR scoped as “Partially addresses #858”.

I’ve prepared a follow-up patch for the second path you identified. It adds canonical shared cache keys with default-scope compatibility aliases (default:reactreact) and applies the shared cache read/write helpers across loadShare, remoteEntry provider bridging, singleton aliasing, host auto-init preload, and react/compiler-runtime.

I’ll open it as a separate PR instead of expanding #861, so this one can stay focused on the optimizeDeps gating fix. Once it’s up, could you re-test it against the same repro?

@gioboa gioboa left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks @dmchoi77 LGTM 👏

@gioboa gioboa merged commit 3bfe17c into module-federation:main Jun 26, 2026
19 checks passed
@gioboa

gioboa commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator

Thanks @abhayzmp for your check.

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.

3 participants