← Back To Blog

Rendering Dynamic Remote Containers in a React Micro Frontend

Jul 5, 20224 min read

How 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 fetched
fallback="Loading..."
// Which remote to fetch the component from
remote="RemoteApp"
// Name of the React component exposed in our remote app
module="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 remote
const remoteUrl = useRemoteUrl(remote);
// Lazy loading the remote component
const Component = React.lazy(loadComponent(remote, remoteUrl, module, scope));
// Wrapping the remote component in an ErrorBoundary and React.Suspense to safely render the component
return (
<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 loaded
if (!(remoteName in window)) {
// Initializes the shared scope. Fills it with known provided modules from this build and all remotes
await __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 app
await fetchedContainer.init(__webpack_share_scopes__[scope]);
}
// 'container' is the remote app
const container = window[remoteName];
// The module pass to get() must match the "exposes" item in our remote app exactly
const factory = await container.get(`./${moduleName}`);
// 'Module' is the React Component from our remote app's "exposes" configuration
const 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 file
const 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 Federation
script.onload = () => {
// The script is now loaded on window using the name defined within the remote
const 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 load
document.head.appendChild(script);
});

Voila! We have a mechanism for reliably fetching and rendering remote apps within our React micro frontend.

Dynamic Remotes

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