Angular micro-frontend library - ngx-mfe
A library for working with MFE in Angular in a plugin-based approach and with Angular routing.
If you have production build issues check this issue. This issue has been fixed in version 2.0.0.
Have problems with updates? Check out the migration guides.
Contents
Version Compliance
Angular | v12.0.0 | v13.0.0 | v13.0.0 | v14.0.0 |
@angular-architects/module-federation | v12.0.0 | v14.0.0 | v14.0.0 | v14.3.0 |
Since v15.0.0 version of ngx-mfe library is compatible with Angular version
Motivation
When Webpack 5 came along and the Module Federation plugin, it became possible to separately compile and deploy code for front-end applications, thereby breaking up a monolithic front-end application into separate and independent MicroFrontEnd (MFE) applications.
The ngx-mfe is an extension of the functionality of the @angular-architects/module-federation. Using @angular-architects/module-federation you could only upload one micro-frontend per page (in the Routing), this limitation was the main reason for the creation of this library.
The key feature of the ngx-mfe library is ability to work with micro-frontends directly in the HTML template using a plugin-based approach. You can load more than one micro-frontend per page.
You can use both ngx-mfe and @angular-architects/module-federation libs together in the same project.
Features
🔥 Load multiple micro-frontend directly from an HTML template with the ability to display a loader component during loading and a fallback component when an error occurs during loading and/or rendering of the mfe component.
🔥 Easy to use, just declare structural directive *mfeOutlet
in your template.
🔥 Supports Angular Standalone Components.
🔥 More convenient way to load MFE via Angular Routing.
🔥 It's easy to set up different remoteEntryUrl MFEs for different builds (dev/prod/etc).
Examples
Conventions
-
To display a standalone MFE component, you only need to the component file itself.
A standalone component is a component that does not have any dependencies provided or imported in the module where that component is declared.
Since Angular v14 standalone component it is component that marked with standalone: true
in @Component({...})
decorator.
When you display a standalone MFE component through [mfeOutlet]
directive you must omit [mfeOutletModule]
input.
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-standalone',
standalone: true,
imports: [CommonModule],
template: ` <p>Standalone component works!</p> `,
styles: [],
})
export class StandaloneComponent {}
{
new ModuleFederationPlugin({
name: 'dashboard-mfe',
filename: 'remoteEntry.js',
exposes: {
StandaloneComponent: 'apps/dashboard-mfe/src/app/standalone.component.ts',
},
[...]
});
}
<ng-template
mfeOutlet="dashboard-mfe"
mfeOutletComponent="StandaloneComponent"
>
</ng-template>
-
To display an MFE component with dependencies in the module where the component was declared, you must expose both the component file and the module file from ModuleFederationPlugin.
This approach is widely used and recommended.
When you display this type of MFE component with the [mfeOutlet]
directive, you must declare an input [mfeOutletModule]
with the value of the exposed module name.
-
The file key of an exposed Module or Component (declared in the ModuleFederationPlugin in the 'expose' property) must match the class name of that file.
For the plugin-based approach, when loads MFE using [mfeOutlet]
directive you must declare Component in the exposed Module and the Component name must match the file key of an exposed Component class.
{
new ModuleFederationPlugin({
name: 'dashboard-mfe',
filename: 'remoteEntry.js',
exposes: {
EntryModule: 'apps/dashboard-mfe/src/app/remote-entry/entry.module.ts',
EntryComponent: 'apps/dashboard-mfe/src/app/remote-entry/entry.component.ts',
},
[...]
});
}
If the name of Module doesn't match, you can specify a custom name for this Module in the @Input() property mfeOutletOptions = { componentName: 'CustomName' }
of [mfeOutlet]
directive, and pass { moduleName: 'CustomName' }
options to the loadMfe()
function;
If the name of Component doesn't match, you can specify a custom name for this Component in the @Input() property mfeOutletOptions = { componentName: 'CustomName' }
of [mfeOutlet]
directive, and pass { moduleName: 'CustomName' }
options to the loadMfe()
function;
-
You must follow the rule that only one Component must be declared for an exposed Module. This is known as SCAM (Single Component Angular Module) pattern.
Configuring
Add the ngx-mfe library to a shared property in the ModuleFederationPlugin inside webpack.config.js file for each application in your workspace.
module.exports = {
[...]
plugins: [
[...]
new ModuleFederationPlugin({
remotes: {},
shared: share({
[...]
"ngx-mfe": {
singleton: true,
strictVersion: true,
requiredVersion: 'auto',
includeSecondaries: true
},
...sharedMappings.getDescriptors(),
}),
library: {
type: 'module'
},
}),
[...]
],
[...]
};
To configure this library, you must import MfeModule.forRoot(options: NgxMfeOptions)
into the root module of the Host app(s) and the root module of the Remote apps in order for Remote to work correctly when running as a standalone application:
For feature modules just import MfeModule
without options, where, you may need the functionality of the library, for example, the MfeOutlet
directive.
For core / app module:
@NgModule({
imports: [
MfeModule.forRoot({
mfeConfig: {
"dashboard-mfe": "http://localhost:4201/remoteEntry.js",
"loaders-mfe": "http://localhost:4202/remoteEntry.js",
"fallbacks-mfe": "http://localhost:4203/remoteEntry.js"
},
preload: ['loaders-mfe', 'fallbacks-mfe'],
loader: {
app: 'loaders',
module: 'SpinnerModule',
component: 'SpinnerComponent',
},
loaderDelay: 500,
fallback: {
app: 'fallbacks',
module: 'MfeFallbackModule',
component: 'MfeFallbackComponent',
},
}),
],
})
export class AppModule {}
For feature module:
@NgModule({
imports: [
MfeModule,
],
})
export class Feature1Module {}
List of all available options:
-
mfeConfig
Sync variant of providing mfeConfig:
object where key is micro-frontend app name specified in ModuleFederationPlugin
(webpack.config.js) and value is remoteEntryUrl string. All data will be sets to MfeRegistry.
Key it's the name same specified in webpack.config.js of MFE (Remote) in option name in ModuleFederationPlugin
.
Value set the following pattern: {url}/{remoteEntrypointFilename}
.
-
url
is the url where the remote application is hosted.
-
remoteEntrypointFilename
is the filename supplied in the remote's webpack configuration.
Example http://localhost:4201/remoteEntry.js
(Deprecated from v15.1.0) You can get MfeRegistry
from DI :
class AppComponent {
constructor(public mfeRegistry: MfeRegistry) {}
}
You can even get instace of MfeRegistry
like this:
const mfeRegistry: MfeRegistry = MfeRegistry.instace;
Async variant of providing mfeConfig:
NOTE: The application will wait for initialization and completes when the promise resolves or the observable completes.
Because under the hood used APP_INITIALIZER
injection token with useFactory that returns Observale or Promise. More about APP_INITIALIZER
Also you can provide mfeConfig with loading it from external resource as Observale<MfeConfig>
or Promise<MfeConfig>
, for this you should provide this type of object:
type NgxMfeAsyncConfig = {
useLoader: (...deps: any[]) => Observable<NgxMfeSyncConfig> | Promise<NgxMfeSyncConfig>;
deps?: any[];
};
For example:
mfeConfig: {
useLoader: (http: HttpClient): Observable<MfeConfig> =>
http.get<MfeConfig>('/manifest.json'),
deps: [HttpClient]
},
-
preload (Optional) - a list of micro-frontend names, their bundles (remoteEntry.js) will be loaded and saved in the cache when the application starts.
Next options are only works in plugin-based approach with MfeOutletDirective
:
-
loaderDelay (Optional) - Specifies the minimum loader display time in ms. This is to avoid flickering when the micro-frontend loads very quickly.
By default is 0.
-
loader (Optional) - Displayed when loading the micro-frontend. Implements the RemoteComponent
interface.
Example:
loader: {
app: 'loaders',
module: 'SpinnerModule',
component: 'SpinnerComponent',
},
For better UX, add loader micro-frontends to the preload
.
-
fallback (Optional) - Displayed when loading or compiling a micro-frontend with an error. Implements the RemoteComponent
interface.
Example:
fallback: {
app: 'fallbacks',
module: 'MfeFallbackModule',
component: 'MfeFallbackComponent',
},
For better UX, add fallback micro-frontends to the preload
.
You can get all configured options by injecting NGX_MFE_OPTIONS
by DI:
class AppComponent {
constructor(@Inject(NGX_MFE_OPTIONS) public options: NgxMfeOptions) {}
}
Display MFE in HTML template / plugin-based approach
This approach allows us to load micro-frontends directly from HTML.
The advantages of this approach are that we can display several MFEs at once on the same page, even display several of the same MFEs.
More about plugin-based approach here.
Full code of this example can be found at https://github.com/dkhrunov/ngx-mfe-test.
Example app:

An example webpack.config.js that exposes the "MfeTestComponent" (brown border in the screenshot above):
return {
[...]
resolve: {
alias: sharedMappings.getAliases(),
},
plugins: [
new ModuleFederationPlugin({
name: 'test',
exposes: {
MfeTestModule: 'apps/test/src/app/mfe-test/mfe-test.module.ts',
MfeTestComponent: 'apps/test/src/app/mfe-test/mfe-test.component.ts',
},
filename: 'remoteEntry',
shared: share({ ... }),
}),
sharedMappings.getPlugin(),
],
};
- Just display the component "MfeTestComponent" inside other MFE component "Form" from "address-form" app:
One variant:
```html
<ng-template
mfeOutlet="test"
mfeOutletModule="MfeTestModule"
mfeOutletComponent="MfeTestComponent"
>
</ng-template>
```
Other variant:
```html
<ng-container
*mfeOutlet="
'test';
module: 'MfeTestModule';
component: 'MfeTestComponent'
"
>
</ng-container>
```
> These two examples are equal and display the MFE "MfeTestComponent".
2. You can pass/bind @Input
and @Output
props to MFE component:
```html
<!-- form.component.html file -->
<ng-container
*mfeOutlet="
'test';
module: 'MfeTestModule';
component: 'MfeTestComponent';
inputs: { text: text$ | async };
outputs: { click: onClick };
"
></ng-container>
```
```typescript
// form.component.ts file
@Component({
selector: 'app-form',
templateUrl: './form.component.html',
styleUrls: ['./form.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FormComponent {
[...]
// timer emits after 1 second, then every 2 seconds
public readonly text$: Observable<number> = timer(1000, 2000);
// on click log to console event
public onClick(event: MouseEvent): void {
console.log('clicked', event);
}
[...]
}
```
> If you try to bind a @Output() property that is not in the component, then an error will fall into the console:
> "Output **someOutput** is not output of **SomeComponent**."
>
> If you try to pass a non-function, then an error will fall into the console:
> "Output **someOutput** must be a function."
3. To override the default loader delay, configured in MfeModule.forRoot({ ... })
, provide custom number in ms to property loaderDelay
:
```html
<ng-container
*mfeOutlet="
'test';
module: 'MfeTestModule';
component: 'MfeTestComponent';
loaderDelay: 1000
"
></ng-container>
```
4. To override the default loader and fallback MFE components, configured in MfeModule.forRoot({ ... })
, specify content with TemplateRef
, pass it to the appropriate properties loader
and fallback
:
```html
<ng-container
*mfeOutlet="
'test';
module: 'MfeTestModule';
component: 'MfeTestComponent';
loader: loaderTpl;
fallback: fallbackTpl
"
></ng-container>
<ng-template #loaderTpl>
<div>loading...</div>
</ng-template>
<ng-template #fallbackTpl>
<div>Ooops! Something went wrong</div>
</ng-template>
```
```html
<!-- TemplateRef that render loader as MFE component -->
<ng-template
mfeOutlet="test"
mfeOutletModule="MfeTestModule"
mfeOutletComponent="MfeTestComponent"
[mfeOutletLoader]="loaderMfeTpl"
></ng-template>
<ng-template #loaderMfeTpl>
<ng-template
mfeOutlet="loaders-mfe"
mfeOutletModule="SpinnerModule"
mfeOutletComponent="SpinnerComponent"
[mfeOutletLoader]="undefined"
[mfeOutletLoaderDelay]="0"
>
</ng-template>
</ng-template>
```
6. You can also provide a custom injector for a component like this:
```html
<ng-template
mfeOutlet="test"
mfeOutletModule="MfeTestModule"
mfeOutletComponent="MfeTestComponent"
[mfeOutletInjector]="customInjector"
></ng-template>
```
Display Angular v14 Standalone components
Example app:

An example webpack.config.js that exposes the "StandaloneComponent" (green border in the screenshot above):
return {
[...]
resolve: {
alias: sharedMappings.getAliases(),
},
plugins: [
new ModuleFederationPlugin({
name: 'test',
exposes: {
[...]
StandaloneComponent: 'apps/test/src/app/standalone/standalone.component.ts',
},
filename: 'remoteEntry',
shared: share({ ... }),
}),
sharedMappings.getPlugin(),
],
};
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-standalone',
standalone: true,
imports: [CommonModule],
template: ` <p>Standalone component works!</p> `,
styles: [],
})
export class StandaloneComponent {}
[...]
<h3>Angular v14 Standalone component loaded as MFE:</h3>
<ng-template
mfeOutlet="test"
mfeOutletComponent="StandaloneComponent"
></ng-template>
Passing Data to the MFE Component via mfeOutlet directive
After using this library for some time, as the author of this library, I came to the conclusion that using @Inputs and @Outputs of an MFE component through the [mfeOutletInputs]
[mfeOutletOutputs]
properties is not the best practice. Try to make your MFE components as independent as possible from the external environment. But if you still have to pass some values to the component, you can do it in two ways:
-
As I wrote above through the properties [mfeOutletInputs]
[mfeOutletOutputs]
component.html:
<ng-template
mfeOutlet="test"
mfeOutletModule="MfeTestModule"
mfeOutletComponent="MfeTestComponent"
[mfeOutletInputs]="{ text: text$ | async }"
[mfeOutletOutputs]="{ click: onClick }"
>
</ng-template>
component.ts
@Component({ ... })
export class Component {
public text$ = new BehaviorSubject<string>('Test string');
constructor() { }
public onClick(bool: MouseEvent): void {
console.log('login', bool);
}
}
-
The second way is to create a new injector and add the necessary data for the MFE component to it. The [mfeOutlet]
directive has the [mfeOutletInjector]
property through which you can pass the desired injector, when the component is created, the previously passed injector in the [mfeOutletInjector]
property will be used instead of the current injector.
component.html:
<ng-template
mfeOutlet="test"
mfeOutletModule="MfeTestModule"
mfeOutletComponent="MfeTestComponent"
[mfeOutletInjector]="testComponentInjector"
>
</ng-template>
component.ts
@Component({ ... })
export class Component {
public readonly testComponentInjector: Injector;
constructor(private readonly _injector: Injector) {
this.testComponentInjector = Injector.create({
parent: this._injector,
providers: [
{
provide: TEST_DATA,
useValue: data,
},
],
});
}
}
Load MFE by Route
To use micro-frontends in Routing, you must import and apply the helper function called loadMfe
, like in the example below:
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { loadMfe } from '@dkhrunov/ng-mfe';
const routes: Routes = [
{
path: 'dashboard',
loadChildren: () => loadMfe('dashboard-mfe', 'EntryModule'),
},
];
@NgModule({
imports: [RouterModule.forRoot(routes, { initialNavigation: 'enabledBlocking' })],
exports: [RouterModule],
})
export class AppRoutingModule {}
Changelog
Changes in v2.1.0
Fixed:
- Fix error, if the fallback is also unavailable, then simply clear the view;
Refactored:
- Renamed
MfeService
to RemoteComponentLoader
;
- Renamed
MfeComponentsCache
to RemoteComponentsCache
;
- Renamed
ModularRemoteComponent
type to RemoteComponentWithModule
;
- Wrapped to
ngZone.runOutside
the loadMfe
function calls inside the RemoteComponentLoader
;
- Added new type
ComponentWithNgModuleRef<TComponent, TModule>
, that holds component class Type<T>
and NgModuleRef
;
- Changed cached value for
RemoteComponentWithModule
from ComponentFactory
to ComponentWithNgModuleRef
;
- In
RemoteComponentLoader
(old name MfeService
) renamed function loadModularComponent
to loadComponentWithModule
- Changed return type of method
loadComponentWithModule
inside class RemoteComponentLoader
from Promise<ComponentFactory<TComponent>>
to Promise<ComponentWithNgModuleRef<TComponent, TModule>>
;
Changes in v2.0.0 (Breaking changes)
Why has the API changed? - The problem is that when you use the [mfeOutlet]
directive issue, it tries to find the component inside the compiled module by name (as a string), but in runtime the class name will be optimized and replaced with a short character. For example, you have a class TestComponent
, it can be changed to the class name a
and this causes this error.
General:
-
To properly use the plugin-based approach in a micro-frontend architecture, or simply if you are use [mfeOutlet]
directive, you must now expose both the component file and module file in which the component is declared to the ModuleFederationPlugin.
Rarerly : or, if your micro-frontend component is standalone (a standalone component is a component that does not have any dependencies declared or imported in the module where that component is declared), then it is sufficient to provide just that component file to the ModuleFederationPlugin;
-
Now ngx-mfe does not use Micro-frontend string
(or anouther name MFE string
) is a kebab-case style string and matches the pattern "mfe-app-name/exposed-file-name"
(it was used until version 2.0.0);
-
MFE string
has been replaced by a new type RemoteComponent
;
-
The validateMfe
function has been removed (it was used until version 2.0.0);
-
The loader
and fallback
properties in the NgxMfeOptions
has been changed from MFE string
to RemoteComponent
type:
Before v2.0.0:
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
BrowserAnimationsModule,
MfeModule.forRoot({
mfeConfig: {
"dashboard-mfe": "http://localhost:4201/remoteEntry.js",
"loaders-mfe": "http://localhost:4202/remoteEntry.js",
"fallbacks-mfe": "http://localhost:4203/remoteEntry.js"
},
loader: 'loaders/spinner',
fallback: 'fallbacks/mfe-fallback',
}),
],
bootstrap: [AppComponent],
})
export class AppModule {}
Since v2.0.0:
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
BrowserAnimationsModule,
MfeModule.forRoot({
mfeConfig: {
"dashboard-mfe": "http://localhost:4201/remoteEntry.js",
"loaders-mfe": "http://localhost:4202/remoteEntry.js",
"fallbacks-mfe": "http://localhost:4203/remoteEntry.js"
},
loader: {
app: 'loaders',
module: 'SpinnerModule',
component: 'SpinnerComponent',
},
fallback: {
app: 'fallbacks',
module: 'MfeFallbackModule',
component: 'MfeFallbackComponent',
},
}),
],
bootstrap: [AppComponent],
})
export class AppModule {}
-
Removed moduleName
property from LoadMfeOptions
type;
-
Now, wherever you need to specify the name of the exposed file through the config in the webpack.config in the ModuleFederationPlugin, you must specify exactly the same name as in the config itself, the kebab-style name was used earlier.
exposes: {
LoginModule: 'apps/auth-mfe/src/app/login/login.module.ts',
},
Before v2.0.0:
loadMfe('auth-mfe/login-module')
Since v2.0.0:
loadMfe('auth-mfe' 'LoginModule')
LoadMfe function:
-
Arguments changed in LoadMfe
function:
Before v2.0.0:
async function loadMfe<T = unknown>(mfeString: string, options?: LoadMfeOptions): Promise<Type<T>> {}
Since v2.0.0:
async function loadMfe<T = unknown>(remoteApp: string, exposedFile: string, options?: LoadMfeOptions): Promise<Type<T>> {}
remoteApp
- is the name of the remote app as specified in the webpack.config.js file in the ModuleFederationPlugin in the name property;
exposedFile
- is the key (or name) of the exposed file specified in the webpack.config.js file in the ModuleFederationPlugin in the exposes property;
MfeOutlet directive:
-
Since the Mfe string
has been removed from the library, the API of [mfeOutlet]
directive has changed:
mfeOutletLoader
and mfeOutletFallback
now accept only TemplateRef
, more details below.
- To load a standalone component, you must specify the following details:
mfeOutlet
with the name of the application, mfeOutletComponent
with the name of the component's open file from the ModuleFederationPlugin in webpack.config. But to load a non-standalone component, you must additionally specify mfeOutletModule
with the name of the open module file in which the component is declared for the ModuleFederationPlugin in webpack.config.
-
@Input('mfeOutletOptions')' options
changed type from MfeComponentFactoryResolverOptions
to LoadMfeOptions
;
-
@Input('mfeOutletLoader')' loader
and @Input('mfeOutletFallback') fallback
now accept only TemplateRef
, not TemplateRef
or Mfe string
. But you can still use micro-frontend component for loader
and fallback
in the [mfeOutlet]
, like in the example below:
<ng-template
mfeOutlet="dashboard-mfe"
mfeOutletModule="EntryModule"
mfeOutletComponent="EntryComponent"
[mfeOutletLoader]="loaderMfe"
>
</ng-template>
<ng-template #loaderMfe>
<ng-template
mfeOutlet="loaders-mfe"
mfeOutletModule="SpinnerModule"
mfeOutletComponent="SpinnerComponent"
[mfeOutletLoader]="undefined"
[mfeOutletLoaderDelay]="0"
>
</ng-template>
</ng-template>
<ng-template
mfeOutlet="dashboard-mfe"
mfeOutletModule="EntryModule"
mfeOutletComponent="EntryComponent"
[mfeOutletLoader]="loaderMfe"
>
</ng-template>
<ng-template #loader>
<div>loading...</div>
</ng-template>
MfeComponentFactoryResolver:
- The
MfeComponentFactoryResolver
has been replaced with MfeService
and the API has been changed;
- The
MfeComponentFactoryResolverOptions
type has been removed;
MfeComponentCache
- Now the
MfeComponentCache
not only saves ComponentFactory<T>
but also Type<T>
;
- In version 2.1.0
ComponentFactory<T>
was replaced to ComponentWithNgModuleRef<TComponent, TModule>
;
DynamicComponentBinding
- The
bindInputs()
and bindOutputs()
methods now require ComponentRef<any>
in the first argument, MfeOutletInputs
/MfeOutletOutputs
are method dependent in the second, and the third argument has been removed;
- The
DynamicComponentInputs
and DynamicComponentOutputs
types have been removed because these types are replaced in bindInputs()
and bindOutputs()
respectively by the ComponentRef<any>
type;
- The
validateInputs()
method has been removed;
- The
validateOutputs()
method is now private;
Changes in v1.1.0:
-
Deleted the loadMfeComponent
helper function;
-
Deleted the parseMfeString
helper function;
-
Renamed the loadMfeModule
helper function to loadMfe
and added optional parameter options: LoadMfeOptions
. LoadMfeOptions
has property a moduleName
, that sets a custom name for the Module class within the opened file, and has type
that specify type of Module Federation;
-
Renamed the MfeService
to MfeComponentFactoryResolver
;
-
MfeComponentFactoryResolver
has the same method as MfeService
, but now it can accepts an optional options: MfeComponentFactoryResolver
parameter. This parameter extends LoadMfeOptions
type, added a componentName
parameter, that sets a custom name for the Component class.
-
Added new Input prop to the MfeOutletDirective
- options: MfeComponentFactoryResolver
, this parameter provided to resolveComponentFactory
method of the MfeComponentFactoryResolver
when resolving the component factory of MFE.
-
Since v1.1.0 you don't need to expose from ModuleFederationPlugin
for plugin-based approach both Module and Component, just specify the Module file.
The exposed Module key must match the name of the exposed module without the 'Module' suffix. Also, if the name doesn't match, you can specify a custom Module name in the options { moduleName: 'CustomName' }
in the property mfeOutletOptions
inside MfeOutletDirective
and in the options parameter of the loadMfe
helper function.
For the plugin-based approach, when loads MFE using MfeOutletDirective
you must declare Component in the exposed Module and the Component name must match the exposed Module key without suffix 'Component'. Also, if the name doesn't match, you can specify a custom Component name in the Input property mfeOutletOptions = { componentName: 'CustomName' }
;
Changes in v1.0.8:
IMfeModuleRootOptions
interface renamed to NgxMfeOptions
;
- Property
delay
in the NgxMfeOptions
renamed to loaderDelay
;
OPTIONS
injection token renamed to NGX_MFE_OPTIONS
;