Nx v23 introduces a new generator surface in @nx/react (@nx/react:consumer and @nx/react:provider) that replaces the v22 host and remote generators. Shared logic (federation-name validation, port defaults, version pins) lives in @nx/module-federation so framework-specific generators can build on it. The new model has two roles:
- Provider - an app that exposes a federated component.
- Consumer - an app that loads federated components at runtime via a hardcoded
PROVIDERSlist insrc/mf.ts.
Both generators are React-only. The bundler is chosen at generation time and cannot be changed later (the bundler config is too different to switch in place).
Pick a bundler
Section titled “Pick a bundler”The generator emits a plain bundler config (no Nx wrapper). Pick one at generation time:
| Bundler | Pick it when |
|---|---|
vite (default) | Fastest dev iteration; smallest config surface; widest ecosystem. |
rsbuild | You want webpack-class production builds with a small config surface. |
rspack | You want full webpack-API compatibility and explicit control of the build. |
Generate a provider
Section titled “Generate a provider”nx g @nx/react:provider apps/my-provider --bundler=viteGenerated tree:
apps/my-provider/├── package.json # name, scripts, MF + framework deps├── vite.config.ts # (or rsbuild/rspack equivalent) with federation plugin├── index.html├── src/│ ├── index.ts # `import('./bootstrap')` indirection (required)│ ├── bootstrap.tsx # createRoot + render│ └── App.tsx # the federated component, default export└── tsconfig.jsonThe federation plugin exposes ./App (or --exposeName=<your-name>). The expose name stays as you type it for the public MF key (consumers reference <provider>/<exposeName>, so cart-widget is fine); the generated React component and its filename are normalized to PascalCase (CartWidget) so the emitted TypeScript is valid. Consumers register the provider by its remoteEntry.js URL. The provider runs standalone on the configured port (default 5101 for Vite, 3101 for Rsbuild, 8101 for Rspack).
Generate a consumer
Section titled “Generate a consumer”nx g @nx/react:consumer apps/my-consumer --bundler=vite --providerNames=my-providerGenerated tree:
apps/my-consumer/├── package.json├── vite.config.ts # NO build-time `remotes:` block├── index.html├── src/│ ├── index.ts # `import('./bootstrap')`│ ├── bootstrap.tsx # createRoot + render <App />│ ├── App.tsx # imports + renders one lazy remote per PROVIDERS entry│ └── mf.ts # hardcoded PROVIDERS list + registerRemotes + lazyProvider helper└── tsconfig.jsonWhen --providerNames=p1,p2,p3 is passed, the consumer generator also scaffolds a sibling @nx/react:provider app per entry (at apps/p1, apps/p2, apps/p3) and wires their remoteEntry.js URLs into the consumer's PROVIDERS list. Each provider's serve target depends on the consumer's serve, so nx serve p1 brings the consumer up alongside the provider.
Omit the flag and the consumer ships a placeholder my-provider entry in PROVIDERS (no actual provider project is generated).
How dynamic federation works
Section titled “How dynamic federation works”The consumer's src/mf.ts holds the remote list inline:
// `name` is the provider's federation container name (derived from its// project name, so it can differ from `alias` - e.g. `myCart` -> `my_cart`).// `alias` is the key you loadRemote() with. `entry` is the remoteEntry.js URL.const PROVIDERS: Array<{ alias: string; name: string; entry: string }> = [ { alias: 'my-provider', name: 'my_provider', entry: 'http://localhost:5101/remoteEntry.js', },];
registerRemotes( // For vite providers only - vite emits ESM remoteEntry.js. The generator // omits `type` for rspack/rsbuild (UMD) so the runtime auto-detects. PROVIDERS.map((remote) => ({ ...remote, type: 'module' })));
export function lazyProvider(alias, exposeName) { /* lazy + loadRemote */}Edit PROVIDERS to point at different providers. The entry is each provider's remoteEntry.js URL - the entry every bundler emits at dev + build time. (The richer mf-manifest.json works for production builds and for rspack/rsbuild dev, but @module-federation/vite only emits it at build time, so the generator uses remoteEntry.js for a consistent dev experience.)
The generated App.tsx renders each provider inside a ProviderBoundary (an inline class that combines Suspense with an error boundary, so one unreachable provider can't unmount the whole tree):
import { lazyProvider } from './mf';// ...ProviderBoundary class omitted...
const ProviderMyProvider = lazyProvider('my-provider', 'App');
export function App() { return ( <main> <h1>my-consumer</h1> <ProviderBoundary name="my-provider"> <ProviderMyProvider /> </ProviderBoundary> </main> );}Wrap each <ProviderBoundary> in your router of choice (TanStack Router, React Router, etc.) if you need routing.
What changed in v23
Section titled “What changed in v23”The following surfaces are deprecated in v23 and will be removed in v24:
@nx/react:host,@nx/react:remote,@nx/react:federate-module@nx/angular:host,@nx/angular:remote,@nx/angular:setup-mf,@nx/angular:federate-module@nx/react:module-federation-dev-server,@nx/react:module-federation-ssr-dev-server,@nx/react:module-federation-static-server@nx/angular:module-federation-dev-server,@nx/angular:module-federation-dev-ssr@nx/rspack:module-federation-dev-server,@nx/rspack:module-federation-ssr-dev-server,@nx/rspack:module-federation-static-server
The most user-visible change: nx serve <host> no longer auto-builds and serves all remotes. With dynamic federation, the relationship is inverted - serve a provider and Nx brings its consumer along (provider.serve.dependsOn = ['<consumer>:serve']). Missing providers don't crash the consumer; they reject at loadRemote time and the generated ProviderBoundary renders an inline fallback.
Angular Module Federation in Nx is no longer supported. Use @angular-architects/native-federation for the supported Angular path going forward.
Migrate from host / remote
Section titled “Migrate from host / remote”There is no automated codemod. Existing setups vary widely (custom executors, host orchestration, SSR variants) and a generator would not land cleanly. Migrate manually using the steps below, or paste the AI prompt at the end of this section into Cursor / Claude Code / Copilot to perform the rewrite on your own codebase.
Manual steps
Section titled “Manual steps”- Generate a fresh
consumerfor each existing host and aproviderfor each existing remote, using the same bundler you were on (or upgrade to Vite). Use a temporary directory so nothing overwrites the originals. - Delete
module-federation.config.tsfrom each app. - Replace the bundler config in each app with the generated one. Port any custom
withModuleFederationwrapping to inline plugin options. - Convert each consumer's static
remotes:list into entries insrc/mf.ts'sPROVIDERSconstant (URLs point at each provider'sremoteEntry.js). - Rewrite
import('remote-name/Module')calls tolazyProvider('remote-name', 'Module')fromsrc/mf.ts. - Drop the
customWebpackConfigblock from eachproject.json. Replace theservetarget with one that runsvite/rsbuild/rspackdirectly vianx:run-commands. - On each provider's
servetarget, adddependsOn: ['<consumer>:serve']if you wantnx serve <provider>to spin up the consumer alongside.
AI prompt
Section titled “AI prompt”Paste this into your AI assistant of choice, adjusted with your project paths:
You are migrating an Nx workspace from the deprecated `@nx/react:host` / `@nx/react:remote` generators to the new`@nx/react:consumer` / `@nx/react:provider` generators (Nx v23+).
For each `host` app in `apps/`:
1. Delete `module-federation.config.ts`.2. Replace the bundler config with a plain `vite.config.ts` / `rsbuild.config.ts` / `rspack.config.ts` that uses `@module-federation/vite` (vite), `@module-federation/rsbuild-plugin` (rsbuild), or `@module-federation/enhanced/rspack` (rspack) directly. No `withModuleFederation` wrappers.3. Create `src/mf.ts` with a `PROVIDERS` constant - an array of `{ alias, name, entry }` where `name` is the provider's federation container name (derived from its project name, so it can differ from `alias`, e.g. `myCart` -> `my_cart`) and `entry` is its `remoteEntry.js` URL. At module init, call `registerRemotes` from `@module-federation/runtime` with one entry per provider (set `type: 'module'` for vite providers; omit it for rspack/rsbuild). Export `lazyProvider(alias, exposeName)` that returns `React.lazy(() => loadRemote(...))`.4. Replace all `import('remote-name/Module')` calls with `lazyProvider('remote-name', 'Module')`.5. Drop the `customWebpackConfig` block from `project.json`. Replace `serve` and `build` targets with `nx:run-commands` invocations of the bundler directly.
For each `remote` app, do the equivalent provider conversion: drop the federation config file, write a plain bundlerconfig with the federation plugin exposing the same modules, and emit a standalone `index.html` + `src/index.ts` ->`src/bootstrap.tsx` indirection. Add `serve.dependsOn: ['<host>:serve']` to the provider's project.json if you want`nx serve <provider>` to also start the consumer.
Do not orchestrate remotes from the host. Each app is served independently; missing remotes render via the consumer'sSuspense + ErrorBoundary.SSR is not first-classed in the new generators. The deprecated module-federation-ssr-dev-server executors are gone. If you need SSR with federation, see the upstream Module Federation SSR guide and wire it yourself on top of the generated consumer skeleton.
Reference
Section titled “Reference”A worked reference workspace covering all three bundlers, dynamic federation, and Angular Native Federation lives at the mf-examples repository (see apps/nx-react-vite/ for the canonical Nx-wired setup).