Working on my latest post regarding module federation here and the work I am doing regarding Module Federation at work brought me to a situation where I was wondering if there is another way to load a remote module, not at build time but runtime; After researching and attending to talks about the subject, I found that this is supported out of the box with Webpack and module federation plug-in.
When I discovered functionality, I was amazed and surprised nobody had told me this before. Now I will share how you can: Dynamically remote modules using Webpack Module Federation at runtime, so for me, "This Is The Way".
Steps required for Dynamic Remote modules
- Configuring the Host App
- Load script from remote module dynamically
- Load Component from
webpack
share scope - Consume remote component from host
- Small peek of remote configuration
- Result
Configuring the Host App
Use ModuleFederationPlugin
in your webpack.config.js
of the app that you wish to consume modules.
- Pay attention that the
remotes
entry now it's an empty object; you can also omit the object. - This is the only change you need regarding configuration now you need some code.
- If you are consuming all dynamically, you can remove the plugin from the configuration
const ModuleFederationPlugin = require('webpack').container.ModuleFederationPlugin;
// your original webpack.config.js configuration
plugins: [
new ModuleFederationPlugin({
name: 'host_react_module',
filename: 'remoteEntry.js',
remotes: {
},
shared: {
react: {
requiredVersion: false,
singleton: true,
},
},
}),
],
Load script from remote module dynamically
- I'm using here a simple
hook
inReact
- This
hook
will create a script element using the browser's native API - After the script element was created we set its properties
import React from "react";
const useDynamicScript = (args) => {
const [ready, setReady] = React.useState(false);
const [failed, setFailed] = React.useState(false);
React.useEffect(() => {
if (!args.url) {
return;
}
const element = document.createElement("script");
element.src = args.url;
element.type = "text/javascript";
element.async = true;
setReady(false);
setFailed(false);
element.onload = () => {
console.log(`Dynamic Script Loaded: ${args.url}`);
setReady(true);
};
element.onerror = () => {
console.error(`Dynamic Script Error: ${args.url}`);
setReady(false);
setFailed(true);
};
document.head.appendChild(element);
return () => {
console.log(`Dynamic Script Removed: ${args.url}`);
document.head.removeChild(element);
};
}, [args.url]);
return {
ready,
failed
};
};
export default useDynamicScript;
Load Component from webpack
share scope
- Use the created
hook
for loading the script - Load the component using
React.lazy
API and webpack functionality
import React, { Suspense } from "react";
import useDynamicScript from './hooks/useDynamicScript';
function loadComponent(scope, module) {
return async () => {
// Initializes the share scope. This fills it with known provided modules from this build and all remotes
await __webpack_init_sharing__("default");
const container = window[scope]; // or get the container somewhere else
// 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();
return Module;
};
}
function ModuleLoader(props) {
const { ready, failed } = useDynamicScript({
url: props.module && props.url
});
if (!props.module) {
return <h2>Not system specified</h2>;
}
if (!ready) {
return <h2>Loading dynamic script: {props.url}</h2>;
}
if (failed) {
return <h2>Failed to load dynamic script: {props.url}</h2>;
}
const Component = React.lazy(
loadComponent(props.scope, props.module)
);
return (
<Suspense fallback="Loading Module">
<Component />
</Suspense>
);
}
export default ModuleLoader;
Consume remote component from host
- Now, after all the parts are set in place, its time to consume the component
- I'm using passing the dynamic parameters thru the URL; this one approach, the easy one, but you can go crazy 🤪 with it and create your own implementation
- Once the app it loaded I'm injecting the parameters from the remote module in the URL
- I'm using a remote module that I already deployed at Vercel, so my URL will look like this:
- http://localhost:8080/?url=https://remote-react-module.vercel.app/RemoteEntry.js&scope=remote_react_module&module=./Kylo
- url: Address of remote module
- scope: name of the remote module set in its webpack config
- module: Component exposed in the remote module
import React, { Suspense, useEffect, useState } from 'react';
import ModuleLoader from './ModuleLoader';
function App() {
useEffect(() => {
const params = new URLSearchParams(window.location.search);
const url = params.get('url');
const scope = params.get('scope');
const module = params.get('module');
setRemote({ url, scope, module });
}, []);
const [remote, setRemote] = useState(null);
return (
<>
<div className='Text'>
This is the React container App hosted at localhost:8080
</div>
<div className='Host-Container'>
<Suspense fallback={'Loading . . . '}>
{
remote && <ModuleLoader url={remote.url} scope={remote.scope} module={remote.module} />
}
</Suspense>
</div>
</>
);
}
export default App;
Small peek of remote configuration
-
In the
webpack
config of the remote module:- Name of the remote module:
remote_react_module
- Expose a component called:
./Kylo
- These parameters MUST match when passing in the URL of the host app ```javascript
plugins: [
new ModuleFederationPlugin({
name: 'remote_react_module',
filename: 'RemoteEntry.js',
exposes: {
'./Kylo': './src/components/Kylo',
},
}),
.
.
. - Name of the remote module:
### 🤯 Result 🤯
![Result of module federation](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tkrfehbzawx7m7021bwj.gif)
### Resources
[Link](https://github.com/OmerHerera/dynamic_remote) to host react using this functionality
Top comments (5)
Thank you very much. I was just looking a similar solution. It really helped me.
Cool!!! Thanks!
Hello, In this example the remote module only works when the page is refreshed. Even if the loaded script is deleted, it is impossible to install and remove the code. how can i remove script code from browser. because I checked the console and saw that it is still installed. how can i make dynamic loading module.
I'm testing this scenario:
It works when I click on the 1st of these buttons. but when I click on other modules it doesn't work. it just loads and the first one I click is loading.
I got the controls. The code loaded with the script is deleted, but the code still remains in the browser.
In this example, can you make the example structure from the state management instead of the url.
I think the ModuleFederation name has to match an entry or something...
Or it might be that the remote cannot use optimizations like chunking. Still figuring out my issue.