Naming conventions for modules and their exports to avoid naming collisions

When using Module Federation you may face some challenges when it comes to naming your modules and their exports. Since you are loading modules from different sources, you need to make sure that they don’t conflict with each other or with the host application. Otherwise, you might end up with unexpected behavior, errors, or broken functionality.

In this guide, we will show you some best practices for naming your modules and their exports when using Module Federation. We will also explain how Webpack handles naming collisions and how you can customize it to suit your needs.

Naming your modules

A module is a unit of code that can be loaded by Webpack. It can be a JavaScript file, a CSS file, an image, or any other type of asset. When using Module Federation, you can expose some of your modules to other bundles or applications by using the exposes option in your Webpack configuration.

For example, suppose you have a module called utils.js that contains some utility functions. You can expose it to other bundles by adding this to your Webpack configuration:

module.exports = {
  // ...
  plugins: [
    new ModuleFederationPlugin({
      // ...
      exposes: {
        "./utils": "./src/utils.js",
      },
    }),
  ],
};

The key of the exposes object is the name of the module that you want to expose. The value is the path to the module file relative to your project root. In this case, we are exposing the module as ./utils and pointing it to ./src/utils.js.

When you expose a module, you need to follow some naming conventions to avoid collisions with other modules. Here are some tips:

  • Use a unique prefix for your modules. This can be based on your project name, domain name, or any other identifier that distinguishes your modules from others. For example, if your project is called my-app, you can prefix your modules with my-app/, like this:

module.exports = {
  // ...
  plugins: [
    new ModuleFederationPlugin({
      // ...
      exposes: {
        "my-app/utils": "./src/utils.js",
      },
    }),
  ],
};
  • Avoid using generic or common names for your modules. For example, don’t expose a module as ./react or ./lodash, as these might conflict with other bundles that use the same libraries. Instead, use more specific names that reflect the purpose or content of your modules. For example, if you have a custom React component library, you can expose it as my-app/components, like this:

module.exports = {
  // ...
  plugins: [
    new ModuleFederationPlugin({
      // ...
      exposes: {
        "my-app/components": "./src/components/index.js",
      },
    }),
  ],
};
  • Use consistent naming conventions across your modules. For example, if you use camelCase for your module names, stick to it throughout your project. Don’t mix it with snake_case or kebab-case. This will make your modules easier to find and import.

Naming your exports

An export is a value or a function that a module provides to other modules. When using Module Federation, you can import exports from other bundles or applications by using the import statement or the import() function.

For example, suppose you want to import the utils module that we exposed earlier from another bundle. You can do it like this:

import utils from "my-app/utils";

Or like this:

import("my-app/utils").then((utils) => {
  // ...
});

The name of the export that you import is the same as the name of the module that you expose. In this case, we are importing the default export of the utils module as utils.

When you export values or functions from your modules, you need to follow some naming conventions to avoid collisions with other exports. Here are some tips:

  • Use descriptive and meaningful names for your exports. For example, don’t export a function as foo or bar, as these might conflict with other functions that have the same name. Instead, use more specific names that reflect what the function does or returns. For example, if you have a function that calculates the area of a circle, you can export it as calculateCircleArea, like this:

export function calculateCircleArea(radius) {
  return Math.PI * radius * radius;
}
  • Use consistent naming conventions across your exports. For example, if you use camelCase for your export names, stick to it throughout your project. Don’t mix it with snake_case or kebab-case. This will make your exports easier to find and import.

  • Avoid using default exports for your modules. Default exports are anonymous and can be imported with any name. This can lead to confusion and inconsistency when importing modules from different sources. For example, if you have a module that exports a React component as the default export, you can import it like this:

  • Approach 1

  • Approach 2

  • Approach 3

import MyComponent from "my-app/components";
import Foo from "my-app/components";
import Bar from "my-app/components";

As you can see, the name of the import does not match the name of the module or the component. This can make it hard to track where the component comes from and what it does. Instead, use named exports for your modules and import them with the same name. For example, if you have a module that exports a React component as a named export, you can export and import it like the following:

  • Export

  • Import

import MyComponent from "my-app/components";
import { MyComponent } from "my-app/components";

This way, the name of the import matches the name of the module and the component. This will make your code more readable and maintainable.

Handling naming collisions

Sometimes, despite following the naming conventions, you might encounter naming collisions with other modules or exports. This can happen when you import modules from different sources that use the same or similar names for their modules or exports.

Webpack provides some options to handle naming collisions and resolve them in a way that suits your needs. Here are some of them:

  • Use aliases to rename modules or exports when importing them. Aliases are alternative names that you can assign to modules or exports when importing them. This can help you avoid conflicts and confusion when dealing with modules or exports that have the same or similar names. For example, suppose you want to import two modules that both expose a utils module. You can use aliases to rename one of them when importing it, like this:

  • Approach 1

  • Approach 2

import utils from "my-app/utils"; // Import utils from my-app
import otherUtils as "other-app/utils"; // Import utils from other-app and rename it as otherUtils
import { utils as myUtils } from "my-app/utils"; // Import utils from my-app and rename it as myUtils
import { utils as otherUtils } from "other-app/utils"; // Import utils from other-app and rename it as otherUtils

This way, you can avoid naming collisions and use both modules without confusion.

  • Use scopes to group modules or exports under a common namespace. Scopes are prefixes that you can add to your module or export names to create a hierarchy or a category for them. This can help you organize your modules or exports and avoid conflicts with other sources that use the same or similar names. For example, suppose you want to expose some modules under a scope called my-app. You can add the scope to your module names when exposing them, like this:

module.exports = {
  // ...
  plugins: [
    new ModuleFederationPlugin({
      // ...
      exposes: {
        "my-app/utils": "./src/utils.js",
        "my-app/components": "./src/components/index.js",
      },
    }),
  ],
};

Then, you can import them with the scope included, like this:

import utils from "my-app/utils"; // Import utils from my-app scope
import components from "my-app/components"; // Import components from my-app scope

This way, you can avoid naming collisions and use your modules without confusion.

  • Use remotes to specify where to load modules from. Remotes are references to other bundles or applications that expose modules using Module Federation. You can use remotes to specify where to load modules from when importing them. This can help you avoid conflicts and confusion when dealing with modules that have the same or similar names but come from different sources. For example, suppose you want to import a module called utils from another bundle called other-app. You can use remotes to specify where to load the module from, like this:

module.exports = {
  // ...
  plugins: [
    new ModuleFederationPlugin({
      // ...
      remotes: {
        // Define a remote called other-app that points to the URL of the other bundle
        other-app: "other-app@https://other-app.com/remoteEntry.js",
      },
    }),
  ],
};

Then, you can import the module with the remote name included, like this:

import utils from "other-app/utils"; // Import utils from other-app remote

This way, you can avoid naming collisions and use the module without confusion.

Conclusion

Naming your modules and their exports when using Module Federation is an important aspect of creating micro-frontends, sharing code across applications, and optimizing performance and scalability. By following some best practices and conventions, you can avoid naming collisions and ensure that your modules and exports are clear, consistent, and easy to use. You can also leverage Webpack’s options to handle naming collisions and resolve them in a way that suits your needs.