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 {}
Enter fullscreen mode Exit fullscreen mode

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 {}
Enter fullscreen mode Exit fullscreen mode

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>)

working_host

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.

working_remote

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 {}
Enter fullscreen mode Exit fullscreen mode

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.

doubled_remote

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),
}
Enter fullscreen mode Exit fullscreen mode

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',
  },
};
Enter fullscreen mode Exit fullscreen mode

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 },
];
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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' })
    ),
  ],
});
Enter fullscreen mode Exit fullscreen mode

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),
  },
];
Enter fullscreen mode Exit fullscreen mode

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:

entry_rendering_graph

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.

Why does the issue stay away with the classic NgModule-based remote setup instead of standalone components?

Because the Nx remotes generator for the traditional ng-module approach doesn't require the entry component to be declared inside 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 }, 
]
Enter fullscreen mode Exit fullscreen mode

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
  }
];
Enter fullscreen mode Exit fullscreen mode

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.

fixed_remote


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:

GitHub logo 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

  1. nx serve shell
  2. on localhost:4200 click on "remote" link: a blue square is rendered
  3. 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:
Enter fullscreen mode Exit fullscreen mode

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.