While experimenting with Nx’s Webpack Module Federation capabilities, I ran into a problem that seemingly nobody has ever tripped over before.
It concerns the duplication of a remote component when its template contains a <router-outlet> and that component is launched as a standalone application.
One explanation for the limited attention it has received might be that it only surfaces within an Angular Standalone Components setup, at least where remotes are concerned.
The issue
I’ll assume you already know how to build a Module Federated Angular app with Nx.
Once the scaffolding is done, you should end up with a host app that serves as the shell for the entire multi-module application, and at least one remote app.
Each is a Micro-Frontend and thus runs independently, the remote included.
This becomes obvious when you run nx serve host_project_name, which spins them up separately and lets you inspect behavior both when the remote gets loaded as a route under the host, and when it boots as its own application.
To keep the setup minimal, the host app is just a single dumb component with a two-line template:
@Component({
selector: 'testapp-root',
template: `
<a routerLink="remote_app">Remote</a>
<router-outlet></router-outlet>
`,
})
export class RemoteEntryComponent {}
You only need an anchor pointing at the remote, plus a router-outlet that hosts the child component’s template.
The remote, in the same fashion, exists purely as a display helper to expose the problem we are dealing with.
@Component({
selector: 'testapp-remote1-entry',
template: `
<div style="background-color: blue; height: 100px; width: 100px; margin: 5px"></div>
`
})
export class RemoteEntryComponent {}
What we have here is nothing more than a blue square div.
And that's all.
So let's examine the served content next.
For the host application, we'll receive only the anchor; clicking it triggers navigation to the remote, which renders directly below the link (and beneath the host's <router-outlet>)
Navigating directly to the standalone remote application, which is typically served on one port above the host's host_port—so http://localhost:4201 in a standard configuration—renders the remote content right away. As anticipated, the blue square appears immediately, and the host's anchor element is absent from the view.
What exactly goes wrong here?
The trouble begins once a <router-outlet> is introduced into the remote app.
That’s a typical setup—microfrontends frequently rely on their own nested routing, complete with child routes and, naturally, a router-outlet where those child views are displayed.
Below is the updated remote component.
@Component({
selector: 'testapp-remote1-entry',
template: `
<div style="background-color: blue; height: 100px; width: 100px; margin: 5px"></div>
<router-outlet>
`
})
export class RemoteEntryComponent {}
This brief animation demonstrates that the component functions perfectly when nested under the host application, yet behaves unexpectedly during direct navigation to its independently hosted version.
Our remote is being rendered on the page twice!
The reason
To figure out why this happens, we have to take a closer look at the inner workings of the tech stack that Nx plugins rely on to set up Webpack Module Federation in Angular projects.
As far as the host application goes, there's nothing particularly complex: it boots up as its own standalone app, and when the need arises, it can pull in external modules by slightly tweaking the standard routing setup:
{
path: 'remote_app',
loadChildren: () =>
loadRemoteModule('rem1', './Routes').then((m) => m.remoteRoutes),
}
Notice that in Angular Router's loadChildren callback, we no longer use the standard import(path/to/lazy_loaded.module) syntax. Instead, the callback invokes loadRemoteModule('remote_module_name', './exposed_routes_path'), which is provided by Nx.
The module-federation.config.js file inside our remote holds the configuration that maps the arguments supplied to that function call:
module.exports = {
name: 'rem1',
exposes: {
'./Routes': 'apps/rem1/src/app/remote-entry/entry.routes.ts',
},
};
In other words, the host Router consults remote's entry.route.ts to determine which child routes are available for rendering.
Here is the automatically scaffolded entry.route.ts file:
export const remoteRoutes: Route[] = [
{ path: '', component: RemoteEntryComponent },
];
One would expect only the empty path '' mapped to the remote's root component—what we often call the entry component.
That setup works well when the remote plays a supporting role inside a larger application.
In that case, the entire remote structure exists merely as a lazy-loaded subtree within the host's routing configuration.
The picture changes completely if the remote must run on its own as a standalone application.
With no parent app to provide the context, it has to be initialized just like a regular Angular project.
The process Nx adopts begins by serving an index.html, which is generated from a template resembling this:
<!DOCTYPE html>
<html lang="en">
<head>...</head>
<body>
<testapp-remote1-entry></testapp-remote1-entry>
</body>
</html>
Instead of the <app-root></app-root> selector that Angular-cli normally attaches to AppComponent, the body now carries the selector of our custom remote entry component.
From there, main.ts is referenced as usual, which in turn hands over to the bootstrap.ts file.
It is within this file that the real bootstrapping occurs—and for an Angular Standalone Components setup (a notion separate from our remote being served in a standalone manner, so don't mix them up), a snippet like this might suffice:
bootstrapApplication(RemoteEntryComponent)
The Nx remotes generator goes one step beyond, exposing to our Router the inner routes we have defined for the given remote:
...
import { appRoutes } from './app/app.routes';
bootstrapApplication(RemoteEntryComponent, {
providers: [
importProvidersFrom(
RouterModule.forRoot(appRoutes, { initialNavigation: 'enabledBlocking' })
),
],
});
The ./app/app.routes path just triggers lazy loading for the same entry.routes file that we’ve already exposed to our host application.
export const appRoutes: Route[] = [
{
path: '',
loadChildren: () =>
import('./remote-entry/entry.routes').then((m) => m.remoteRoutes),
},
];
Now the issue surfaces!
As we've established, this file maps the root path '' to render the remote's root component.
Yet, when the remote runs standalone, that same component appears because it is referenced in the remote's index.html template.
The diagram below illustrates the situation clearly:
As the purple highlighted labels show, the journey to render the entry component for standalone remote serving runs into two separate spots that request it.
This entry component is, in fact, both declared and routed.
Because the Nx remotes generator for the traditional ng-module approach doesn't require the entry component to be declared inside Why does the issue stay away with the classic NgModule-based remote setup instead of standalone components?
index.html.
Instead, it declares a dedicated root component (AppComponent) that contains a <router-outlet> and brings in RouterModule within the root module (AppModule).
Consequently, the same route that points to the entry component gets rendered correctly only once, whether you're dealing with the Shell setup or with independent remote serving.
My solution
There are probably multiple ways to address this, given that any logical fix involves reworking Nx generators for remotes that rely on Angular standalone components.
My approach is to separate the roles of the remote's app.routes and entry.routes.
Currently, the former just lazily loads the latter, which results in the same initial routing behavior for both serving contexts.
Instead, I proposed making app.routes the true route configuration for the remote, while entry.routes serves only as a "plugin" activated in the Shell context, preserving the required root path to the entry component and incorporating app.routes as its child routes.
To achieve this, we start by updating app.routes—removing its lazy load of entry.routes and adding our remote child routes directly into its array.
To keep entry.routes within our compilation unit, we relocate its import outside the routes definition, ensuring it remains compiled and exposed via our federation configuration, but stays unused by our router during independent serving:
/* commented-out original default entry.routes import
export const appRoutes: Route[] = [
{
path: '',
loadChildren: () =>
import('./remote-entry/entry.routes').then((m) => m.remoteRoutes),
},
]; */
import('./remote-entry/entry.routes')
export const appRoutes: Route[] = [
{ path: 'first_child_route', component: FirstChildRouteComponent },
{ path: 'second_child_route', component: SecondChildRouteComponent },
{ path: 'third_child_route', component: ThirdChildRouteComponent },
]
It’s time to modify entry.routes. The single route it currently contains can be extended with a children array—importing the route set from app.routes is all that’s needed.
import { appRoutes } from '../app.routes';
import { RemoteEntryComponent } from './entry.component';
export const remoteRoutes: Route[] = [
{
path: '',
component: RemoteEntryComponent,
children: appRoutes
}
];
Once all the pieces are in place, we can finally confirm that our blue square appears exactly once, regardless of which serving approach we picked.
Conclusions
The state of Nx support for Angular standalone component federation strikes me as somewhat immature.
There were a couple of quirks in its implementation that I might touch on in a later post.
Still, it does a solid job, saving developers from tons of boilerplate code, and given my lack of deep expertise here, it's entirely plausible that what I perceived as "defects" were actually deliberate workarounds for edge cases or typical workflows I hadn't considered.
For that same reason, the "fix" I proposed might not be ideal—or could even be wrong—in different contexts.
Since I haven't put it through rigorous testing, I'd strongly urge you to drop a comment if anything seems off.
In any case, I've filed a bug report for this issue and submitted a PR containing my proposed fix:
Duplicate entry component rendering for standalone ng component served as independent frontend
#14551
Current Behavior
When generating angular standalone component as remote, the entry component is listed inside entry.routes for root path ''.
That's fine when module is lazy loaded by shell app, but gives a problem when served as independent microfrontend.
In that case the entry component is declared into index.html too, so if a router-outlet gets added somewhere in the tree, the component will be rendered twice.
Expected Behavior
When served as independent app, entry.routes should be ignored, and inner routes should be defined into remote's app.routes directly.
Github Repo
https://github.com/4javier/monotest
Steps to Reproduce
nx serve shell- on
localhost:4200click on "remote" link: a blue square is rendered - on
localhost:4201: two squares get rendered
Nx Report
Node : 14.20.0
OS : linux x64
npm : 6.14.17
nx : 15.5.1
@nrwl/angular : 15.5.1
@nrwl/cypress : 15.5.1
@nrwl/detox : Not Found
@nrwl/devkit : 15.5.1
@nrwl/esbuild : Not Found
@nrwl/eslint-plugin-nx : 15.5.1
@nrwl/expo : Not Found
@nrwl/express : Not Found
@nrwl/jest : 15.5.1
@nrwl/js : 15.5.1
@nrwl/linter : 15.5.1
@nrwl/nest : Not Found
@nrwl/next : Not Found
@nrwl/node : Not Found
@nrwl/nx-cloud : Not Found
@nrwl/nx-plugin : Not Found
@nrwl/react : Not Found
@nrwl/react-native : Not Found
@nrwl/rollup : Not Found
@nrwl/schematics : Not Found
@nrwl/storybook : Not Found
@nrwl/web : Not Found
@nrwl/webpack : 15.5.1
@nrwl/workspace : 15.5.1
@nrwl/vite : Not Found
typescript : 4.8.4
---------------------------------------
Local workspace plugins:
---------------------------------------
Community plugins:
Failure Logs
No response
Additional Information
I explained extensively the issue and my suggested solution here. https://dev.to/this-is-angular/nx-module-federation-bad-angular-routing-1ac9
Cheers.





