Transforming Your Application into Micro Frontends with Native Federation for Angular — Part 3
Learn how to start structuring your application to work with MFE using Native Federation for Angular
Angular with Native Federation
In the first article, we learned how to set up the initial configuration of the projects and got them running.
In our second article, we delved deeper into routes and how to resolve Angular Router issues for Micro Frontends (MFE). Now, let’s move forward and learn how to effectively export components and services.
Important: For everything to work, I used version 18.0.0 of @angular-architects/native-federation
. Currently, version 18.0.2 has a bug that prevents dynamic importation.
Importing Dynamic Components
To start, I have prepared a component within mfe2
that we will export: animated-box.component
. This component contains an animated box and is already being used within mfe2
. However, we now need to import it for use within mfe1
. So, how do we do that?
Step 1: Export the Component in the federation.config
First, we will export our animated-box
component in the federation.config
:
// federation.config.ts
exposes: {
'./Component': './src/app/app.component.ts',
'./AnimatedBox': './src/app/components/animated-box/animated-box.component.ts',
}
Step 2: Create a Placeholder for the Component in mfe1
Next, in our mfe1
, we will create a div to act as a placeholder where the component will be inserted. In crud.component.html
:
<div #placeAnimatedBox></div>
Step 3: Load the Remote Component
Now, let’s proceed to our crud.component.ts
and perform the actual remote loading of the component:
import { loadRemoteModule } from '@angular-architects/native-federation';
import { Component, OnInit, ViewChild, ViewContainerRef } from '@angular/core';
import { RouterModule } from '@angular/router';
import { CustomRouterLinkDirective } from '../directives/custom-router-link.directive';
@Component({
selector: 'app-crud',
standalone: true,
imports: [RouterModule, CustomRouterLinkDirective],
templateUrl: './crud.component.html',
styleUrls: ['./crud.component.scss']
})
export class CrudComponent implements OnInit {
@ViewChild('placeAnimatedBox', { read: ViewContainerRef })
viewContainer!: ViewContainerRef;
constructor() { }
ngOnInit() {
setTimeout(() => {
this.loadAnimatedBox();
}, 2000);
}
async loadAnimatedBox(): Promise<void> {
const m = await loadRemoteModule({
remoteEntry: 'http://localhost:4202/remoteEntry.json',
exposedModule: './AnimatedBox'
});
const ref = this.viewContainer.createComponent(m.AnimatedBoxComponent);
// const compInstance = ref.instance;
}
}
And just like magic, we can import a component created and maintained within mfe2
into mfe1
:
Resolving Service Issues in Angular Micro Frontends
Our component works because it is extremely simple. However, if this component starts to depend on a service, it will no longer function. To resolve this, let’s change our approach slightly by creating a service within mfe2
.
Creating the Service in mfe2
First, we define a service inside mfe2
:
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
@Injectable()
export class DataService {
constructor(private httpClient: HttpClient) { }
fetchData() {
return this.httpClient.get('https://jsonplaceholder.typicode.com/posts/1');
}
}
Providing the Service in App Component
Next, we need to provide this service in the app.component.ts
:
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { ExposeAnimatedBoxComponent } from './exposes/expose-animated-box/expose-animated-box.component';
import { FooComponent } from './pages/foo/foo.component';
import { DataService } from './services/data.service';
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet, FooComponent, ExposeAnimatedBoxComponent],
providers: [DataService],
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
title = 'mfe2';
}
Since we are using HttpClient
, we must define the provider in app.config.ts
. It's also recommended to do the same in our mfe1
and shell
to ensure an instance of HttpClient
is available when importing the component.
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient, withFetch } from '@angular/common/http';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes),
provideHttpClient(withFetch())
]
};
Using the Service in a Component
Now, let’s call fetchData
inside our animated-box.component.ts
:
import { CommonModule } from '@angular/common';
import { Component, OnInit } from '@angular/core';
import { DataService } from '../../services/data.service';
@Component({
selector: 'app-animated-box',
standalone: true,
imports: [CommonModule],
templateUrl: './animated-box.component.html',
styleUrls: ['./animated-box.component.scss']
})
export class AnimatedBoxComponent implements OnInit {
constructor(private dataService: DataService) { }
ngOnInit() {
this.dataService.fetchData().subscribe((data) => {
console.log("AnimatedBoxComponent: " + JSON.stringify(data));
});
}
}
With this setup, the integration with the endpoint is functioning inside mfe2
.
Handling Errors in mfe1
However, in mfe1
, we encounter an error:
To resolve this, instead of exporting our AnimatedBoxComponent
directly, we create a separate component that will provide all the necessary modules, services, and providers that our AnimatedBoxComponent
needs. I suggest creating a separate folder for these intermediate components, such as /exposes
.
Creating ExposeAnimatedBoxComponent
Inside the /exposes
folder, let's create a component ExposeAnimatedBoxComponent
:
<!-- expose-animated-box.component.html -->
<app-animated-box></app-animated-box>
// expose-animated-box.component.html
import { Component } from '@angular/core';
import { AnimatedBoxComponent } from '../../components/animated-box/animated-box.component';
import { DataService } from '../../services/data.service';
@Component({
selector: 'app-expose-animated-box',
standalone: true,
imports: [AnimatedBoxComponent],
providers: [DataService],
templateUrl: './expose-animated-box.component.html',
styleUrl: './expose-animated-box.component.scss'
})
export class ExposeAnimatedBoxComponent {
}
We will then export our component that encapsulates the necessary dependencies for AnimatedBoxComponent
:
exposes: {
'./Component': './src/app/app.component.ts',
'./ExposeAnimatedBox': './src/app/exposes/expose-animated-box/expose-animated-box.component.ts',
},
Updating crud.component.ts
Finally, in our crud.component.ts
, let's update the import to use the new component:
import { loadRemoteModule } from '@angular-architects/native-federation';
import { Component, OnInit, ViewChild, ViewContainerRef } from '@angular/core';
import { RouterModule } from '@angular/router';
import { CustomRouterLinkDirective } from '../directives/custom-router-link.directive';
@Component({
selector: 'app-crud',
standalone: true,
imports: [RouterModule, CustomRouterLinkDirective],
templateUrl: './crud.component.html',
styleUrls: ['./crud.component.scss']
})
export class CrudComponent implements OnInit {
@ViewChild('placeAnimatedBox', { read: ViewContainerRef })
viewContainer!: ViewContainerRef;
constructor() { }
ngOnInit() {
setTimeout(() => {
this.loadAnimatedBox();
}, 2000);
}
async loadAnimatedBox(): Promise<void> {
const m = await loadRemoteModule({
remoteEntry: 'http://localhost:4202/remoteEntry.json',
exposedModule: './ExposeAnimatedBox'
});
const ref = this.viewContainer.createComponent(m.ExposeAnimatedBoxComponent);
}
}
With this approach, we successfully encapsulate the AnimatedBoxComponent
within another (ExposeAnimatedBoxComponent
) that serves to provide the necessary services and modules, ensuring they are properly provided at the root or the top level of our application tree in mfe2
.
With Great Power Comes Great Responsibility
The biggest advantage of micro front ends is the ability to break down a monolithic application into smaller, more manageable parts, allowing independent teams to develop, deploy, and maintain these parts in isolation. This increases scalability, facilitates maintenance, and promotes the reuse of components. Additionally, it allows for the adoption of different technologies as needed, improving development flexibility.
By using the approach of remote component loading in Angular and micro front ends, it is possible to implement functionalities and components maintained by other teams (squads). This approach has the advantage of automatically reflecting updates made by the team responsible for the component. However, this advantage can also lead to problems if proper care is not taken with the maintenance of exported components. It is crucial to ensure that exported components are stable and compatible to avoid failures or inconsistencies in the application.
Bonus
When analyzing the size of the applications individually and then loaded within the shell, we observe that they are not fully reloaded. This is because Native Federation does an excellent job of reusing dependency code. Thus, remote loading is restricted to only the code that was actually written for your application, optimizing performance and reducing load time. This efficiency in dependency management allows applications to be lighter and faster, improving the user experience.
For a practical demonstration and to follow along with the implementation details, you can check out the complete source code on GitHub. The repository includes all the necessary configurations and code examples to help you set up and run the project seamlessly. Visit the repository here.
Conclusion
Breaking down a monolithic application into micro front ends brings various benefits, such as scalability, easier maintenance, and technological flexibility. By using remote component loading in Angular, we can automatically reflect updates made by other teams, provided there is care with the stability and compatibility of exported components. Native Federation optimizes performance by reusing dependency code, resulting in lighter and faster applications.
Follow me on LinkedIn: https://www.linkedin.com/in/erickzanetti