Joining the (Module) Federation
Lev Eidelman Nagar
Posted on February 28, 2024
Introduction
Module federation is a way of loading remote modules in runtime (as opposed to during the build process) and is a common way of implementing micro-frontends in modern web applications which use webpack.
In this guide I will show you how you can load any remote module at runtime.
I assume that you have at least passing familiarity with the basics of webpack and module federation.
If this is a new topic for you I highly recommend reading the official webpack documentation and visiting module-federation.io/ for more information.
When You Can't Commit
Most module federation examples and tutorials out there will have you configure module federation for your host application like so:
// webpack.config.js
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'host',
remotes: {
app1: 'app1@http://localhost:3001/remoteEntry.js',
// ...additional remotes
},
}),
],
};
While this will work locally, it does not take into account the following:
- Different environments(e.g. staging, production)
- Needing to load an arbitrary remote from an unknown(at least during build time) URL
Although the first can be solved by using external-remotes-plugin, handling arbitrary remotes is less obvious.
I should also note that configuring the remotes this way means that all remotes will be loaded when the page initially starts. This means the user will incur the cost of having to load additional Javascript files even if they never use any of it.
A Little Helper Goes a Long Way
I created a small utility function that accepts the URL of the remote module or a promise that resolves to it, loads the remote script by creating a new DOM <script>
element, initializes the module and returns it.
type RemoteAppSettings = {
scope: string;
module: string;
};
/**
* Loads a remote application module
* See [Dynamic Remote Containers](https://webpack.js.org/concepts/module-federation/#dynamic-remote-containers)
* for more information on how this works
* @param url Remote app URL or function that resolved to a URL
* @param app Remote app settings
* @returns Remote module
*/
export function loadRemoteApp(
url: string | (() => Promise<string>),
app: RemoteAppSettings
) {
const { scope, module } = app;
return (): Promise<{ default: ComponentType<any> }> =>
new Promise(async (resolve, reject) => {
const element = document.createElement('script');
let resolvedUrl: string;
if (typeof url === 'string') {
resolvedUrl = url;
} else {
resolvedUrl = await url();
}
element.src = resolvedUrl;
element.type = 'text/javascript';
element.async = true;
element.onload = async () => {
// Initializes the share scope.
const container = window[scope];
// Initialize the container, it may provide shared modules
await container.init(__webpack_share_scopes__.default);
const factory = await window[scope].get(module);
const Module = factory();
resolve(Module);
};
element.onerror = err => {
reject(
new Error(
`Failed to initialize remote Application\nURL: ${url}\nScope: ${scope}\nModule: ${module}`,
{ cause: err }
)
);
};
document.head.appendChild(element);
});
}
Now we can remove all pre-configured remotes from our webpack.config.js
:
// webpack.config.js
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'host',
remotes: {},
}),
],
};
And in our React application we can use this helper to load a remote component and render it with lazy
and <Suspense>
like this:
import { getRemoteUrl, loadRemoteApp } from 'common/utils/moduleFederation';
import ErrorBoundary from 'components/ErrorBoundary';
import { Loader } from 'connected-components/Loader/Loader';
import { ComponentType, Suspense, lazy } from 'react';
import { RemoteComponentProps } from 'types/micro-frontends';
const RemotApp = lazy<ComponentType<RemoteComponentProps>>(
loadRemoteApp(
// Call the backend to get the remote URL
getRemoteUrl('component-name'),
{
scope: 'remote',
module: './App'
}
)
);
export function RemoteComponent() {
return (
<ErrorBoundary>
<Suspense fallback={<Loader />}>
<RemoteApp/>
</Suspense>
</ErrorBoundary>
);
}
Now the remote module will not be loaded until required and we can use any valid remote URL
.
Posted on February 28, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.