Skip to content

feat(client): expose host React to Module Federation remotes - #2828

Merged
james-elicx merged 2 commits into
cloudflare:mainfrom
kdy1:kdy1/fix-module-federation-react-bridge
Aug 6, 2026
Merged

feat(client): expose host React to Module Federation remotes#2828
james-elicx merged 2 commits into
cloudflare:mainfrom
kdy1:kdy1/fix-module-federation-react-bridge

Conversation

@kdy1

@kdy1 kdy1 commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add a browser-only vinext/client entry point with getVinextReact() so Module Federation remotes can reuse the host React instance
  • register the host React instance before App Router and Pages Router client runtimes, including Pages Router development hydration
  • document the required singleton sharing configuration, usage, and browser-only limitations

Why

Module Federation remotes can evaluate with a different React module instance from the vinext host. Components that read hooks from that copy can hit React dispatcher mismatches and fail with invalid hook call errors.

The client bridge stores the first browser React instance under a global symbol and returns it to remotes explicitly. First-registration semantics also keep the host instance stable when duplicate modules are evaluated or HMR reloads the bridge. Server and RSC environments continue to use their condition-specific React instances without global registration.

User impact

Client-side Module Federation integrations can import getVinextReact from vinext/client and resolve hooks against the vinext host React instance. Consumers still need compatible singleton react, react/, react-dom, and react-dom/ sharing configuration. This does not add App Router Module Federation SSR or rewrite React imports in third-party packages.

Validation

  • pnpm test tests/client-react-bridge.test.ts tests/entry-templates.test.ts tests/deploy.test.ts — 369 tests passed
  • pnpm run check — formatting, lint, type checks, Next.js type sync, and public shim checks passed

@kdy1
kdy1 force-pushed the kdy1/fix-module-federation-react-bridge branch from 856013d to 91dba70 Compare August 6, 2026 11:29
@kdy1 kdy1 changed the title fix(client): bridge React for Module Federation feat(client): expose host React to Module Federation remotes Aug 6, 2026
@kdy1
kdy1 marked this pull request as ready for review August 6, 2026 11:31

@Nsttt Nsttt left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This will hep us a lot to leverage https://github.com/module-federation/vinext/

@pkg-pr-new

pkg-pr-new Bot commented Aug 6, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@vinext/cloudflare@2828
npm i https://pkg.pr.new/create-vinext-app@2828
npm i https://pkg.pr.new/@vinext/types@2828
npm i https://pkg.pr.new/vinext@2828

commit: cf76f1d

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Performance benchmarks

Compared 91dba70 against base 3c5cea4 using alternating same-runner rounds. Next.js was unchanged and skipped.

0 improved · 0 regressed · 6 within ±1.5%

Scenario Framework Baseline Current Change
Client bundle size (gzip) vinext 134.6 KB 134.7 KB ⚫ +0.0%
Client entry size (gzip) vinext 122.2 KB 122.2 KB ⚫ +0.0%
Dev server cold start vinext 3.18 s 3.17 s ⚫ -0.4%
Production build time vinext 3.45 s 3.43 s ⚫ -0.6%
RSC entry closure size (gzip) vinext 113.9 KB 113.9 KB ⚫ -0.0%
Server bundle size (gzip) vinext 191.4 KB 191.4 KB ⚫ +0.0%

View detailed results and traces

🟢 improvement · 🔴 regression · ⚫ change below 1.5% · paired base/head

@james-elicx

Copy link
Copy Markdown
Member

/bigbonk review for issues

@ask-bonk ask-bonk Bot left a comment

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.

Reviewed the Module Federation client React bridge. The implementation is clean, well-scoped, and nicely tested. "First-registration-wins" plus the typeof window === "undefined" server guard is the right shape, and the ordering tests for both routers give good coverage. A couple of questions/nits below — none are blockers.

Verified:

  • dist/client/index.js will be emitted (vite.config.ts uses entry: ["src/**/*.ts"] with unbundle: true), so the new ./client export path resolves.
  • The server-side guard prevents accidental RSC/SSR global registration, and the bootstrap is only imported from client entries, so the side effect runs in the browser as intended.
  • Ordering is guaranteed by textual import position (ESM evaluates static imports top-to-bottom), matching what the ordering tests assert.

Main question: the "use client" directive on react-instance-bootstrap.ts — see inline comment. I don't think it's harmful, but it may be unnecessary and slightly misleading.

@@ -0,0 +1,6 @@
"use client";

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.

Is the "use client" directive actually needed here? This module is only imported directly by the browser/client entries (app-browser-entry.ts, pages-client-entry.ts, pages-dev-hydration.ts), which are already client-side. It's never consumed as a client-reference boundary from the RSC graph.

Unlike the shims in shims/*.tsx that use "use client" (which are imported across the RSC boundary and need the plugin to generate client-reference proxies), this is a pure side-effect module with no exports. Adding "use client" makes @vitejs/plugin-rsc treat it as a client boundary unnecessarily, and it's inconsistent with the other files in src/client/ (e.g. navigation-runtime.ts) which don't carry the directive.

If there's a concrete reason it's required (e.g. a specific env where the bootstrap gets pulled through the RSC transform), a one-line comment explaining it would help. Otherwise consider dropping it.

* Server and RSC environments always keep their condition-specific React
* instance local.
*/
export function getVinextReact(reactInstance: typeof import("react")): typeof import("react") {

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.

Minor robustness note (non-blocking): getVinextReact implements a first-writer-wins singleton keyed on Symbol.for("vinext.client.react"). If a statically-hoisted or dynamically-imported remote ever evaluates and calls getVinextReact(remoteReact) before the host bootstrap runs, the remote's React becomes the registered singleton — the opposite of the intent documented in the README ("vinext registers the host's browser React instance before application modules execute").

In practice the ordering holds because the bootstrap import is textually first in every client entry, so this is fine for the supported cases. Worth keeping in mind if App Router MF SSR / earlier remote evaluation lands later — the "host always wins" guarantee is really "whoever registers first wins", which happens to be the host today.

@ask-bonk

ask-bonk Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Review posted successfully to PR #2828.

Summary

I reviewed the Module Federation client React bridge PR. Overall it's a clean, well-scoped, well-tested change. I posted a review with 2 non-blocking inline comments:

Verified as correct:

  • dist/client/index.js will be emitted by the build (entry: ["src/**/*.ts"] + unbundle: true), so the new ./client package export resolves in packed builds.
  • The typeof window === "undefined" guard correctly prevents accidental global registration in RSC/SSR environments.
  • Import ordering (bootstrap first) is guaranteed by ESM's top-to-bottom static import evaluation, matching the ordering tests in entry-templates.test.ts.

Feedback raised:

  1. "use client" on react-instance-bootstrap.ts — questioned whether it's needed. The module is only imported directly from already-client entries and has no exports (not a client-reference boundary), unlike the shims/*.tsx that use the directive. It's inconsistent with other src/client/ modules like navigation-runtime.ts. Suggested dropping it or adding a comment if it's required.
  2. First-writer-wins semantics — noted that the "host always wins" guarantee is really "whoever registers first wins." It holds today because the bootstrap import is textually first, but worth keeping in mind if App Router MF SSR / earlier remote evaluation lands later.

Neither is a blocker; the PR is in good shape.

github run

@james-elicx
james-elicx enabled auto-merge (squash) August 6, 2026 14:42
@james-elicx
james-elicx merged commit 86620d5 into cloudflare:main Aug 6, 2026
55 checks passed
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