Rendering Dynamic Remote Containers in a React Micro Frontend
Jul 5, 2022 ☕ 4 min readHow can we safely, efficiently, and performantly use Webpack Module Federation’sDynamic Remote Containers in a React micro frontend app?
Let’s start with a vision of what we want:
<RemoteComponent// Text displayed while the component is being fetchedfallback="Loading..."// Which remote to fetch the component fromremote="RemoteApp"// Name of the React component exposed in our remote appmodule="HelloWorld"/>
RemoteComponent
is a React component we can use throughout our application to render modules from a remote app. It encapsulates:
ErrorBoundary
to safely render remote code without breaking our host app.- Lazy loading to resolve the remote code as needed without blocking the rest of our app rendering.
- Fetching and managing dynamic remote containers.
Here is the implementation:
const RemoteComponent = ({remote,module,scope = "default",fallback = null,// Any props needed by the Remote app can be passed through...props}) => {// Custom hook for getting the URL for a particular remoteconst remoteUrl = useRemoteUrl(remote);// Lazy loading the remote componentconst Component = React.lazy(loadComponent(remote, remoteUrl, module, scope));// Wrapping the remote component in an ErrorBoundary and React.Suspense to safely render the componentreturn (<ErrorBoundary><React.Suspense fallback={fallback}><Component {...props} /></React.Suspense></ErrorBoundary>);};
We are lazy loading Component
which we get back from loadComponent()
and wrapping it around ErrorBoundary
to prevent the entire application from crashing if something goes wrong. While fetching the remote app, we use React.Suspense
to render a loading indicator optionally.
useRemoteUrl()
is a React hook managing the URLs of our remote apps. The details aren’t crucial for this example, but you could implement URL management in many different ways depending on your use case. The critical piece is that we can get a URL to pass to the loadComponent
utility that manages the remote containers:
export const loadComponent =(remoteName, remoteUrl, moduleName, scope = "default") =>async () => {// Check if this remote has already been loadedif (!(remoteName in window)) {// Initializes the shared scope. Fills it with known provided modules from this build and all remotesawait __webpack_init_sharing__(scope);// Fetch the remote app. We assume our remote app is exposing a `remoteEntry.js` file.const fetchedContainer = await fetchRemote(`${remoteUrl}/remoteEntry.js`,remoteName);// Initialize the remote appawait fetchedContainer.init(__webpack_share_scopes__[scope]);}// 'container' is the remote appconst container = window[remoteName];// The module pass to get() must match the "exposes" item in our remote app exactlyconst factory = await container.get(`./${moduleName}`);// 'Module' is the React Component from our remote app's "exposes" configurationconst Module = factory();return Module;};
We only load the remote app once, even if we reuse its components throughout our application. The container we fetch must match the get/init
interface expected by Module Federation. This works best if our remote app’s Webpack configuration uses the Module Federation plugin.
fetchRemote()
handles fetching the remote app's remoteEntry.js
file. First, it creates a new script tag and then injects it into the DOM to fetch the remote javascript:
export const fetchRemote = (url, remoteName) =>new Promise((resolve, reject) => {// We define a script tag to use the browser for fetching the remoteEntry.js fileconst script = document.createElement("script");script.src = url;script.onerror = (err) => {reject(new Error(`Failed to fetch remote: ${remoteName}`));};// When the script is loaded we need to resolve the promise back to Module Federationscript.onload = () => {// The script is now loaded on window using the name defined within the remoteconst proxy = {get: (request) => window[remoteName].get(request),init: (arg) => {try {return window[remoteName].init(arg);} catch (e) {console.error(`Failed to initialize remote: ${remoteName}`);reject(e);}},};resolve(proxy);};// Lastly we inject the script tag into the document's head to trigger the script loaddocument.head.appendChild(script);});
Voila! We have a mechanism for reliably fetching and rendering remote apps within our React micro frontend.
Check out a live example here: https://micro-frontend-demo-main.vercel.app/
Source code: link
For other dynamic remote app strategies check out one of my other posts: Dynamic Remote Apps in a Micro Frontend