Micro frontends in Angular with Native Federation: Resolving Common Issues
Micro frontend architecture provides a powerful solution when we have control over both the main app and its associated micro applications. It allows us to build applications as separate, smaller modules that work cohesively. Each module can be developed by different teams and deployed independently. The discrete pieces, or “remotes,” are combined into a cohesive “host application” (also called a “shell application”).
In this article, we’ll explore how to implement micro frontends in Angular using Native Federation and tackle common issues faced during configuration.
Understanding Host, Remote, and Dynamic-Host
Before diving into implementation, let’s clarify the key types of applications in micro frontends:
• Host Application:
The main application that serves as a container for remotes. It defines the routes or components where remote modules will be loaded dynamically.
• Remote Application:
These are the smaller, independent applications that expose specific modules, components, or features for the host to consume.
• Dynamic-Host Application:
A dynamic-host is a host application capable of dynamically loading remotes at runtime.
In Angular Native Federation, the key difference between a host and a dynamic host lies in how they handle remote module loading:
Host:
• Static Configuration: The host application knows about the remote modules it will load at build time.
• Configuration in webpack.config.js
: The configuration for loading remotes is typically defined within the webpack.config.js
file of the host application.
• Build-Time Dependency: The host application's build process includes the necessary information about the remote modules.
Dynamic Host:
• Runtime Configuration: The host application determines which remote modules to load at runtime.
• Configuration in a Separate File: The configuration for loading remotes is usually stored in a separate JSON file (e.g., federation.manifest.json
).
• Flexibility: This approach offers greater flexibility, as the host can dynamically adapt to changes in the available remote modules without requiring a rebuild.
In essence:
• A host has a predefined set of remote modules that it loads during the build process.
• A dynamic host can load different remote modules based on runtime conditions, making it more adaptable and flexible.
By using a dynamic host, you can centralize the management of remote module configurations, making it easier to update and maintain.
In summary:
Choose a host or dynamic host based on your specific needs and the level of flexibility required in your application. If you have a fixed set of remote modules that won’t change frequently, a static host might be sufficient. However, if you need to dynamically load different remote modules based on user interactions or other runtime conditions, a dynamic host provides a more adaptable solution.
Module Federation vs. Native Federation
Module Federation
Module Federation, introduced in Webpack 5, revolutionized micro frontend architecture by allowing dynamic runtime loading of modules between applications. However, it requires Webpack as the bundler, which may not align with modern applications using faster alternatives like esbuild or Vite.
Native Federation
Native Federation offers a bundler-agnostic solution built on web standards like ECMAScript modules and Import Maps. It integrates seamlessly with Angular CLI, reducing boilerplate and enhancing the developer experience. It is the recommended approach for projects using Angular 16 and above.
Native Federation Configuration
Let’s walk through configuring Native Federation for two projects: main-front (host) and micro-front (remote).
• Install the Native Federation package:
npm install @angular-architects/native-federation@18.2.7 --save-dev
Note: Use a version of @angular-architects/native-federation
compatible with your Angular version to avoid dependency errors.
• Initialize Native Federation for the host application (main-front):
ng g @angular-architects/native-federation:init --project main-front --port 4200 --type dynamic-host
• Repeat the initialization for the remote application (micro-front):
ng g @angular-architects/native-federation:init --project micro-front --port 4201 --type remote
• Add an exception for the generated federation.config.js
file in your .eslintrc.json
:
"ignorePatterns": ["federation.config.js"]
Configuring the Federation Files
For both projects, the federation.config.js
file must be configured carefully to avoid dependency resolution issues.
Host Application (main-front
)
module.exports = withNativeFederation({
shared: {
'@angular/core': { singleton: true, strictVersion: false },
'@angular/common': { singleton: true, strictVersion: false },
'@angular/common/http': { singleton: true, strictVersion: false },
'@angular/router': { singleton: true, strictVersion: false },
'@angular/platform-browser': { singleton: true, strictVersion: false },
'@angular/core/primitives/signals': { singleton: true, strictVersion: false },
'@angular/core/primitives/event-dispatch': { singleton: true, strictVersion: false },
},
});
Remote Application (micro-front
)
module.exports = withNativeFederation({
name: 'micro-front',
exposes: {
'./Component': './src/app/app.component.ts',
},
shared: {
'@angular/core': { singleton: true, strictVersion: false },
'@angular/common': { singleton: true, strictVersion: false },
'@angular/common/http': { singleton: true, strictVersion: false },
'@angular/router': { singleton: true, strictVersion: false },
'@angular/platform-browser': { singleton: true, strictVersion: false },
'@angular/core/primitives/signals': { singleton: true, strictVersion: false },
'@angular/core/primitives/event-dispatch': { singleton: true, strictVersion: false },
},
});
Resolving Common Issues
During setup, you may encounter errors that need careful handling. Here are a few common issues and their resolutions:
• Issue 1: 404 Errors for Remote Modules
Error example:
GET http://localhost:4201/Component.js 404 (Not Found)
Solution:
Ensure the exposed components in the federation.config.js file of the remote app match the requested path in the host app.
• Issue 2: Dependency Injection Errors
Error example:
NG0203: inject() must be called from an injection context…
Solution:
Make sure shared dependencies are configured identically in both host and remote applications. Explicitly list the shared libraries instead of using shareAll to avoid dependency conflicts.
• Issue 3: Missing Angular Primitives
Error example:
Unable to resolve specifier ‘@angular/core/primitives/signals’…
Solution:
Add missing Angular primitives (like @angular/core/primitives/signals) to the shared dependencies in both federation.config.js files.
Key Takeaways
To avoid dependency issues:
• Explicitly share libraries instead of relying on shareAll.
• Maintain version parity for shared libraries across host and remote applications.
• Carefully review federation.config.js in both host and remote apps to ensure consistent configurations.
Loading Remote Modules in Angular: Dynamic and Routing Approaches with Native Federation
When working with Micro-Frontend architecture, Native Federation enables seamless loading of remote modules without additional tools like Webpack Module Federation. Angular supports this natively, allowing dynamic integration of remote components. This article explains two approaches: adding a remote module to routing and dynamically loading it in a component.
• Adding the Remote Component to the Routing Module
With Native Federation, you can load remote modules directly in the AppRoutingModule using the loadComponent method for your routes. Here’s an example:
const routes: Routes = [
{
path: 'micro-test',
loadComponent: () =>
loadRemoteModule({
remoteEntry: 'http://localhost:4201/remoteEntry.json',
remoteName: 'micro-front',
exposedModule: './Component',
}).then((m) => m.AppComponent),
},
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
})
export class AppRoutingModule {}
Key Points:
• remoteEntry
: Specifies the entry point of the remote module.
• remoteName
: Refers to the name of the remote application.
• exposedModule
: Identifies the specific module or component to expose.
This method provides a clean, router-integrated experience for remote modules.
• Dynamically Loading the Remote Component in a Component
For more granular control, you can dynamically load a remote component into a ViewContainerRef.
Here’s an example of a RemoteRendererComponent:
export class RemoteRendererComponent implements OnInit, OnDestroy {
@ViewChild('remoteContainer', { read: ViewContainerRef, static: true })
container!: ViewContainerRef;
private componentRef: ComponentRef<any> | null = null;
isLoading = true;
error: string | null = null;
constructor(private microfrontendService: MicrofrontendService) {} async ngOnInit() {
try {
this.isLoading = true;
this.error = null; // Specify the port of the remote module
const port = '4201'; // Dynamically load the remote component
const module = await this.microfrontendService.loadRemoteComponent(port);
const componentType = module.AppComponent; // Clear the container and load the component
this.container.clear();
this.componentRef = this.container.createComponent(componentType);
this.componentRef.changeDetectorRef.detectChanges();
} catch (error) {
console.error('Failed to load remote component:', error);
this.error = 'Failed to load remote component. Please try again later.';
} finally {
this.isLoading = false;
}
} ngOnDestroy() {
if (this.componentRef) {
this.componentRef.destroy();
}
}
}
• The MicrofrontendService
Here’s the service for loading a remote module dynamically:
@Injectable({
providedIn: 'root',
})
export class MicrofrontendService {
async loadRemoteComponent(port: string) {
try {
return await loadRemoteModule({
exposedModule: './Component',
remoteName: 'micro-front',
remoteEntry: `http://localhost:${port}/remoteEntry.json`,
fallback: 'unauthorized',
});
} catch (err) {
console.error('Error loading remote component:', err);
throw err;
}
}
}
Highlights of the Service:
• Dynamically constructs the remoteEntry URL based on the port.
• Supports error handling with the fallback property.
• Ensures the remoteName and exposedModule are configurable.
Conclusion
Native Federation offers a robust solution for Angular micro frontends, especially when predefined remote entry points are feasible. With proper configurations and dependency management, you can achieve seamless integration between host and remote applications.
While it may not support dynamic manifests natively, this approach is ideal for projects with fixed or predictable remote URLs.