DEV Community

Cover image for Dynamic micro frontends with Nx and React
Taras Protchenko
Taras Protchenko

Posted on • Originally published at taras.one

Dynamic micro frontends with Nx and React

When there are a lot of teams on the project, when dynamic frontend expansion is necessary, and when a rebuild of the entire project is not an option, the concept of Micro Frontends comes into play in conjunction with Dynamic Module Federation.

Nx has a great tutorial for angular stack on this topic. Let's try to implement this concept for react stack.

The Nx documentation says:

Nx is a smart, fast and extensible build system with first class monorepo support and powerful integrations.

Now we will check it in practice, we will generate several applications and a helper library.

Create Nx workspace

To create Nx workspace, run the command:

npx create-nx-workspace@latest
Enter fullscreen mode Exit fullscreen mode

Choose a name and type (apps), Nx Cloud can be left unconnected.

Generation of host-app and children apps

Install @nrwl/react plugin as dev dependency. It provides handy generators and utilities that make it easy to manage React apps and libraries inside the Nx workspace.

npm install -D @nrwl/react
Enter fullscreen mode Exit fullscreen mode

Create host-app and micro frontends:

npx nx g @nrwl/react:host host --remotes=cart,blog,shop
Enter fullscreen mode Exit fullscreen mode

Select the styling settings you need in applications and wait for the end of the generation.

Creating a library for easy registration and import of micro frontends

To import micro frontends dynamically by URL, we need to create a library that will help with this. To do this, we will generate a library using the @nrwl/js generator and call it load-remote-module.

npx nx g @nrwl/js:library load-remote-module
Enter fullscreen mode Exit fullscreen mode

Let's add the code to the freshly generated library /libs/load-remote-module/src/lib/load-remote-module.ts:

export type ResolveRemoteUrlFunction = (
  remoteName: string
) => string | Promise<string>;

declare const __webpack_init_sharing__: (scope: 'default') => Promise<void>;
declare const __webpack_share_scopes__: { default: unknown };

let resolveRemoteUrl: ResolveRemoteUrlFunction;

export function setRemoteUrlResolver(
  _resolveRemoteUrl: ResolveRemoteUrlFunction
) {
  resolveRemoteUrl = _resolveRemoteUrl;
}

let remoteUrlDefinitions: Record<string, string>;

export function setRemoteDefinitions(definitions: Record<string, string>) {
  remoteUrlDefinitions = definitions;
}

let remoteModuleMap = new Map<string, unknown>();
let remoteContainerMap = new Map<string, unknown>();

export async function loadRemoteModule(remoteName: string, moduleName: string) {
  const remoteModuleKey = `${remoteName}:${moduleName}`;
  if (remoteModuleMap.has(remoteModuleKey)) {
    return remoteModuleMap.get(remoteModuleKey);
  }

  const container = remoteContainerMap.has(remoteName)
    ? remoteContainerMap.get(remoteName)
    : await loadRemoteContainer(remoteName);

  const factory = await container.get(moduleName);
  const Module = factory();

  remoteModuleMap.set(remoteModuleKey, Module);

  return Module;
}

function loadModule(url: string) {
  return import(/* webpackIgnore:true */ url);
}

let initialSharingScopeCreated = false;

async function loadRemoteContainer(remoteName: string) {
  if (!resolveRemoteUrl && !remoteUrlDefinitions) {
    throw new Error(
      'Call setRemoteDefinitions or setRemoteUrlResolver to allow Dynamic Federation to find the remote apps correctly.'
    );
  }

  if (!initialSharingScopeCreated) {
    initialSharingScopeCreated = true;
    await __webpack_init_sharing__('default');
  }

  const remoteUrl = remoteUrlDefinitions
    ? remoteUrlDefinitions[remoteName]
    : await resolveRemoteUrl(remoteName);

  const containerUrl = `${remoteUrl}${
    remoteUrl.endsWith('/') ? '' : '/'
  }remoteEntry.js`;

  const container = await loadModule(containerUrl);
  await container.init(__webpack_share_scopes__.default);

  remoteContainerMap.set(remoteName, container);
  return container;
}
Enter fullscreen mode Exit fullscreen mode

This code is based on code from the Nx plugin for angular.

Register the load-remote-module library in our host-application /apps/host/webpack.config.js:

const withModuleFederation = require('@nrwl/react/module-federation');
const moduleFederationConfig = require('./module-federation.config');

const coreLibraries = new Set([
  'react',
  'react-dom',
  'react-router-dom',
  '@microfrontends/load-remote-module',
]);

module.exports = withModuleFederation({
  ...moduleFederationConfig,
  shared: (libraryName, defaultConfig) => {
    if (coreLibraries.has(libraryName)) {
      return {
        ...defaultConfig,
        eager: true,
      };
    }

    // Returning false means the library is not shared.
    return false;
  },
});
Enter fullscreen mode Exit fullscreen mode

Registration is required to avoid the error: Uncaught Error: Shared module is not available for eager consumption.

Configuration and Connecting micro frontends

Let's save a list of links to our micro frontends in JSON file format - this is one of the easiest methods to get them at runtime, on the host-app side, all that remains is to make a GET request. In the future, we may use the server API for this purpose.

Create a file module-federation.manifest.json in folder /apps/host/src/assets/module-federation.manifest.json:

{
  "cart": "http://localhost:4201",
  "blog": "http://localhost:4202",
  "shop": "http://localhost:4203"
}
Enter fullscreen mode Exit fullscreen mode

Open /apps/host/src/main.ts and change for:

import { setRemoteDefinitions } from '@microfrontends/load-remote-module';
import('./bootstrap');

fetch('/assets/module-federation.manifest.json')
  .then((res) => res.json())
  .then((definitions) => setRemoteDefinitions(definitions))
  .then(() => import('./bootstrap').catch((err) => console.error(err)));
Enter fullscreen mode Exit fullscreen mode

As you can see, we:

  • Fetch JSON file
  • Call setRemoteDefinitions with its contents
  • This allows webpack to understand where our micro frontends are deployed

Change the method of loading micro frontends in the host-app to dynamic

At the moment, webpack determines where the micro frontends are located during the build step, as it is specified in the /apps/host/module-federation.config.js config file.

Open module-federation.config.js, which is located in our host-app folder /apps/host/module-federation.config.js, and set the value of remotes to an empty array so that webpack does not look for modules when building. It will look like this:

module.exports = {
  name: 'host',
  remotes: [],
};
Enter fullscreen mode Exit fullscreen mode

Next, we need to change the way micro frontends are loaded in our host-app. Open the file /apps/host/src/app/app.tsx and replace the import code with:

import { loadRemoteModule } from '@microfrontends/load-remote-module';

const Cart = React.lazy(() => loadRemoteModule('cart', './Module'));

const Blog = React.lazy(() => loadRemoteModule('blog', './Module'));

const Shop = React.lazy(() => loadRemoteModule('shop', './Module'));
Enter fullscreen mode Exit fullscreen mode

That's all it takes to replace Static Module Federation to Dynamic Module Federation.

Serve and check

To serve our host-app and micro frontends:

npm run start
Enter fullscreen mode Exit fullscreen mode

Or the parallel start of all apps:

nx run-many --parallel --target=serve --projects=host,cart,blog,shop --maxParallel=100
Enter fullscreen mode Exit fullscreen mode

Open localhost:4200 and see, what our micro frontends Dynamic Module Federation is working:

  • config is fetching from module-federation.manifest.json via GET request
  • if you remove one of the applications from it, then we will get an error in the browser
  • we can add additional micro frontends

GitHub repository - dynamic-micro-frontends-with-Nx-and-react.

Additional info:

Big thanks to ScorIL for the help with the load-remote-module library.

Top comments (0)

nextjs tutorial video

Youtube Tutorial Series 📺

So you built a Next.js app, but you need a clear view of the entire operation flow to be able to identify performance bottlenecks before you launch. But how do you get started? Get the essentials on tracing for Next.js from @nikolovlazar in this video series 👀

Watch the Youtube series

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay