Transforming Your Application into Micro Frontends with Native Federation for Angular — Part 3

Erick Zanetti
6 min readAug 5, 2024

--

Learn how to start structuring your application to work with MFE using Native Federation for Angular

Angular with Native Federation

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:

MFE exported components

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.

Fetch data works inside mfe2

Handling Errors in mfe1

However, in mfe1, we encounter an error:

MFE1 with erros

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.

All works

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.

Size example

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

--

--

Erick Zanetti
Erick Zanetti

Written by Erick Zanetti

Full Stack developer with angular and Java. Learning Go.

Responses (1)