← Back To Blog

4 Ways to Use Dynamic Remotes in Module Federation

Jul 6, 20227 min read

How can you deploy the same micro frontend application to testing, staging, and production environments? How can your application support on-premise, cloud, and hybrid deployments simultaneously? How can you scale multiple teams working on different parts of the architecture simultaneously? To introduce new remote applications dynamically?

Why use Dynamic Remotes?

In a previous post, we discussed configuring a simple React application using Webpack Module Federation in a host-remote configuration. When we defined the URLs for the remote applications, we used localhost URLs:

plugins: [
new ModuleFederationPlugin({
name: "Host",
remotes: {
RemoteA: `RemoteA@http://localhost:4000/remoteEntry.js`,
RemoteB: `RemoteB@http://localhost:5000/remoteEntry.js`,
},
}),
];

The above works great for local development, but this would not work if we deployed and hosted the application. Of course, we could replace the strings with production URLs - but then what happens to local development? Do we keep flip-flopping back and forth manually, hoping no one merges in the wrong URLs?

This pattern would also not work if you have multiple deployment stages with different URLs and environments. If we could not test remote apps in isolated environments in deployment pipelines, we would run into chaos and frustration as developers introduce breaking changes. Everyone would lose all confidence in the development flow.

Instead of localhost we want to define URLs dynamically to point to the correct host depending on the environment.

Dynamic Remotes in Webpack Module Federation

Luckily Webpack Module Federation supports dynamically defining URLs for our remote applications. We are going to consider four solutions available to us:

Environment Variables

The most straightforward choice is using environment variables at build time. We replace localhost in our Webpack config with environment variables locally or a deployment pipeline:

module.exports = (env) => ({
// ...
plugins: [
new ModuleFederationPlugin({
name: "Host",
remotes: {
RemoteA: `RemoteA@${env.A_URL}/remoteEntry.js`,
RemoteB: `RemoteB@${env.B_URL}/remoteEntry.js`,
},
}),
],
});

You can define each remote app's URL as a local or hosted production deployment at build time. The advantage of this approach is its' simplicity, but there is a problem. We need to build a new version for each environment to update the URLs. For our enterprise use cases and multiple deployment models, this would not work.

What if you could change the remote URLs at runtime?

Webpack External Remotes Plugin

Under the hood, Module Federation allows us to load remote containers dynamically. We'll cover that in a minute. But first, there is a handy Webpack plugin developed by Zack Jackson, one of the creators of Module Federation, called external-remotes-plugin. It allows us to resolve the URLs at runtime using templating:

plugins: [
new ModuleFederationPlugin({
name: "Host",
remotes: {
RemoteA: "RemoteA@[window.appAUrl]/remoteEntry.js",
RemoteB: "RemoteB@[window.appBUrl]/remoteEntry.js",
},
}),
new ExternalTemplateRemotesPlugin(),
],

All you need to do is define window.appAUrl and window.appBUrl within our application before you load any code from the remote applications.

Now you have the flexibility to define our URLs however you want. For example, you could maintain a map of the host URLs to remote URLs for each environment, use server-side rendering to allow the backend to define the URLs, create a custom configuration management service, and many other methods.

This approach is fully dynamic and would solve our use cases, but there is still a slight limitation with this approach. We do not have complete control over the loading lifecycle.

Promise Based Dynamic Remotes

Module Federation allows us to define the remote URLs as promises instead of URL strings. You can use any promise as long as it fits the get/init interface defined by Module Federation:

// Webpack config:
module.exports = {
// ...
plugins: [
new ModuleFederationPlugin({
name: "Host",
remotes: {
RemoteA: `promise new Promise(${fetchRemoteA.toString()})`,
},
}),
],
};
// Fetch Remote A dynamically:
const fetchRemoteA = (resolve) => {
// We define a script tag to use the browser for fetching the remoteEntry.js file
const script = document.createElement("script");
script.src = window.appAUrl; // This could be defined anywhere
// When the script is loaded we need to resolve the promise back to Module Federation
script.onload = () => {
// The script is now loaded on window using the name defined within the remote
const module = {
get: (request) => window.RemoteA.get(request),
init: (arg) => {
try {
return window.RemoteA.init(arg);
} catch (e) {
console.log("Remote A has already been loaded");
}
},
};
}
resolve(module);
}
// Lastly we inject the script tag into the document's head to trigger the script load
document.head.appendChild(script);
}

Within the promise, we create a new script tag and inject it into the DOM to fetch the remote javascript file. window.appAUrl contains the URL for the remote app.

The Webpack remote configuration must still be in string format, so I've defined the promise separately from the config and stringified it later for better readability. Unfortunately, this approach is not the easiest to debug or maintain because it is stringified code inside of a config file.

There is another limitation with all of the approaches so far. We are limited to only loading the RemoteA and RemoteB apps, which could be a security advantage. But what if a developer is working on a new remote app that we don't know exists yet? What if a partner or customer wants to inject their remote module into their deployment of our app?

Dynamic Remote Containers

Module Federation allows us to load remote applications programmatically without needing to define any URLs in our Webpack config:

plugins: [
new ModuleFederationPlugin({
name: "Host",
remotes: {},
}),
],

But how?

Similar to the above approach, before you try to load any remote apps, you first need to fetch the remote module using a dynamic script tag. Then you can manually initialize the remote container.

From the Webpack docs:

(async () => {
// Initializes the shared scope. Fills it with known provided modules from this build and all remotes
await __webpack_init_sharing__("default");
const container = window.someContainer; // or get the container somewhere else
// Initialize the container, it may provide shared modules
await container.init(__webpack_share_scopes__.default);
const module = await container.get("./module");
})();

container is referring to a remote app we configured in the "remotes" field in our host app's webpack configuration.

module refers to one of the items we defined in the "exposes" field in our remote app's webpack configuration.

Using the approach of injecting a script tag you can fetch the remote container and store it in window.someContainer as long as the code resolves to the same get/init pattern we used earlier.

When you want to use one of the modules exposed by our remote app all you need to do is call: container.get(moduleName) like in the example above.

What does this look like if you want to load a React remote app like we did in our basic React host-remote app?

Check out one of my other posts for an example: Rendering Dynamic Remote Containers in a React Micro Frontend

Final Thoughts

Using dynamically loaded remotes, you've learned four different mechanisms to break down your micro frontend deployment. When you deploy our micro frontend, you can fetch our remote applications from any URL you define. You can deploy it to multiple test environments, on-premises, or in the cloud. Developers can choose whether to use production versions of other remote applications or to introduce new ones dynamically.

Which method would you choose?

Resources

  • Webpack docs: link
  • external-remotes-plugin: link
  • Example of using Environment Variables: link