Mastering Micro Frontends: Creating Scalable Applications with React, Webpack 5, and TypeScript Type Safety

serifcolakel

Serif COLAKEL

Posted on August 13, 2023

Mastering Micro Frontends: Creating Scalable Applications with React, Webpack 5, and TypeScript Type Safety

Author: Serif Colakel
Code: Github Repository Link

Embark on a transformative journey into the realm of micro frontends with our comprehensive article, "Mastering Micro Frontends: Creating Scalable Applications with React, Webpack 5, and TypeScript Type Safety." Delve into the cutting-edge approach that is reshaping modern web development, and learn how to build dynamic and scalable applications that are designed for maintainability, collaboration, and efficiency. Whether you're a seasoned developer or new to the field, this guide will equip you with the knowledge and tools to embrace the power of micro frontends and harness their potential for your projects.

What Are Micro Frontends?

Micro frontends are a new approach to web development that allows developers to build applications in a modular fashion. This means that each component of the application is developed independently, and then combined into a single application at runtime. This approach has many benefits, including increased maintainability, scalability, and collaboration.

🎉 Let's get started!

Create a Container App With create-mf-app CLI

You can follow these steps:

  • Create a new folder. For example, micro-front-template. You can use the following command to create a new folder:
mkdir micro-front-template
Enter fullscreen mode Exit fullscreen mode
  • Navigate to the project directory: Run the following command to navigate into your project directory:
cd micro-front-template
Enter fullscreen mode Exit fullscreen mode
  • Create a new project: Run the following command to create a new project:
npx create-mf-app container
Enter fullscreen mode Exit fullscreen mode

Question 1: Pick the name of your app: (host) : write container and press Enter.
Question 2: Project Type: (Use arrow keys) : select Application and press Enter.
Question 3: Port number: (8080) : write your port number and press Enter. For example, 3000 by default it is 8080.
Question 4: Framework: (Use arrow keys) : select React and press Enter.
Question 5: Language: (Use arrow keys) : select typescript and press Enter.
Question 6: CSS: (Use arrow keys) : select CSS and press Enter.

  • Navigate to the project directory: Run the following command to navigate into your project directory:
cd container
Enter fullscreen mode Exit fullscreen mode
  • Install the packages: Run the following command to install:
npm i
Enter fullscreen mode Exit fullscreen mode
  • Add the tailwind: Run the following command to add the tailwind:
npm install -D tailwindcss

npx tailwindcss init
Enter fullscreen mode Exit fullscreen mode

-- Add the tailwind config file: Run the following command to add the tailwind config file:

// tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: ["./src/**/*.{js,jsx,ts,tsx}"],
  theme: {
    extend: {},
  },
  plugins: [],
};
Enter fullscreen mode Exit fullscreen mode
  • Add the tailwind css file: Run the following command to add the tailwind css file:
/* ./src/index.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
Enter fullscreen mode Exit fullscreen mode
  • Add the tailwind css file to the index.tsx: Run the following command to add the tailwind css file to the index.tsx:
// ./src/index.tsx
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
// ...
Enter fullscreen mode Exit fullscreen mode

Create a Remote App With create-mf-app CLI

You can follow these steps:

  • Create a new folder. For example, micro-front-template. You can use the following command to create a new folder:
mkdir micro-front-template
Enter fullscreen mode Exit fullscreen mode
  • Navigate to the project directory: Run the following command to navigate into your project directory:
cd micro-front-template
Enter fullscreen mode Exit fullscreen mode
  • Create a new project: Run the following command to create a new project:
npx create-mf-app remote
Enter fullscreen mode Exit fullscreen mode

Question 1: Pick the name of your app: (host) : write remote and press Enter.
Question 2: Project Type: (Use arrow keys) : select Application and press Enter.
Question 3: Port number: (8080) : write your port number and press Enter. For example, 3001 by default it is 8080.
Question 4: Framework: (Use arrow keys) : select React and press Enter.
Question 5: Language: (Use arrow keys) : select typescript and press Enter.
Question 6: CSS: (Use arrow keys) : select CSS and press Enter.

  • Navigate to the project directory: Run the following command to navigate into your project directory:
cd remote
Enter fullscreen mode Exit fullscreen mode
  • Install the packages: Run the following command to install:
npm i
Enter fullscreen mode Exit fullscreen mode
  • Add the tailwind: Run the following command to add the tailwind:
npm install -D tailwindcss

npx tailwindcss init
Enter fullscreen mode Exit fullscreen mode

-- Add the tailwind config file: Run the following command to add the tailwind config file:

// tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: ["./src/**/*.{js,jsx,ts,tsx}"],
  theme: {
    extend: {},
  },
  plugins: [],
};
Enter fullscreen mode Exit fullscreen mode
  • Add the tailwind css file: Run the following command to add the tailwind css file:
/* ./src/index.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
Enter fullscreen mode Exit fullscreen mode
  • Add the tailwind css file to the index.tsx: Run the following command to add the tailwind css file to the index.tsx:
// ./src/index.tsx
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";

// ...
Enter fullscreen mode Exit fullscreen mode

Create a UI Components from the Container App

You can follow these steps:

  • Download the clsx & class-variance-authority package: Run the following command to download the package in the container & remote folder:
cd container
npm i clsx class-variance-authority
Enter fullscreen mode Exit fullscreen mode
cd remote
npm i clsx class-variance-authority
Enter fullscreen mode Exit fullscreen mode
  • Create a Button.tsx component in the src/components folder.
import React, { ComponentProps } from "react";
import { type VariantProps, cva } from "class-variance-authority";
import clsx from "clsx";

type ButtonElementProps = ComponentProps<"button">;

export interface ButtonProps
  extends ButtonElementProps,
    VariantProps<typeof buttonStyles> {
  label?: string;
  icon?: React.ReactNode;
  rightIcon?: React.ReactNode;
  buttonWrapperClassName?: string;
}

const buttonStyles = cva(
  "flex text-xs group flex-row gap-x-2 items-center justify-center disabled:!bg-gray-800",
  {
    variants: {
      buttonType: {
        primary: "bg-blue-500 text-white hover:bg-blue-600",
        secondary: "bg-black text-white hover:bg-white hover:text-black",
        error: "bg-error-600 text-white hover:bg-error-700",
        warning: "bg-warning-500 text-white hover:bg-warning-600",
        success: "bg-success-500 text-white hover:bg-success-600",
        info: "bg-blue-500 text-white hover:bg-blue-600",
        default: "bg-gray-400 text-white hover:bg-gray-500",
      },
      size: {
        default: "h-[38px]",
        sm: "h-8 !w-8",
        lg: "h-12",
        xl: "h-14",
        xxl: "h-16",
      },
      padding: {
        default: "px-5 py-2",
        sm: "px-3 py-2",
        lg: "px-5 py-3",
        xl: "px-5 py-4",
        xxl: "px-5 py-5",
      },
      rounded: {
        default: "rounded-lg",
        sm: "rounded-sm",
        lg: "rounded-lg",
        xl: "rounded-xl",
        xxl: "rounded-2xl",
        none: "rounded-none",
        full: "rounded-full",
      },
      isFullWidth: {
        true: "!w-full",
        false: "w-auto",
      },
    },
    compoundVariants: [{ buttonType: "primary", size: "default" }],
    defaultVariants: {
      buttonType: "primary",
      size: "default",
      rounded: "default",
      padding: "default",
      isFullWidth: false,
    },
  }
);

function Button({
  label,
  buttonType,
  rounded,
  padding,
  size,
  isFullWidth,
  className,
  ...buttonProps
}: ButtonProps) {
  return (
    <div className={buttonProps.buttonWrapperClassName}>
      <button
        className={clsx(
          className,
          buttonStyles({
            buttonType,
            rounded,
            padding,
            size,
            isFullWidth,
          })
        )}
        type="button"
        {...buttonProps}
      >
        {buttonProps.icon && <div>{buttonProps.icon}</div>}
        {!!label && <label className="cursor-pointer">{label}</label>}
        {buttonProps.rightIcon && <div>{buttonProps.rightIcon}</div>}
      </button>
    </div>
  );
}

export default Button;
Enter fullscreen mode Exit fullscreen mode

Webpack Configuration for Applications

  • In the container folder, webpack.config.js file should be like this:
const HtmlWebPackPlugin = require("html-webpack-plugin");
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");

// INFO (serif) : Your webpack config should be like this.
const configs = {
  appName: "container",
  appFileName: "remoteEntry.js",
  development: {
    PUBLIC_PATH: "http://localhost:3000/",
    REMOTE_PATH: "remote@http://localhost:3001/remoteEntry.js",
    PORT: 3000,
  },
  production: {
    PUBLIC_PATH: "http://localhost:3000/",
    REMOTE_PATH: "remote@http://localhost:3001/remoteEntry.js",
    PORT: 3000,
  },
};

// INFO (serif) : Define the package.json dependencies.
const deps = require("./package.json").dependencies;

module.exports = (env, argv) => {
  console.log({ env, argv, configs: configs[argv.mode] });

  return {
    output: {
      // INFO (serif) : The output path for the build files.
      publicPath: configs[argv.mode].PUBLIC_PATH,
    },

    resolve: {
      // INFO (serif) : The extensions that should be used to resolve modules.
      extensions: [".tsx", ".ts", ".jsx", ".js", ".json"],
    },

    // INFO (serif) : The configuration for the dev server.
    devServer: {
      hot: true, // INFO (serif) : Enable webpack's Hot Module Replacement feature
      port: configs[argv.mode].PORT,
      historyApiFallback: true,
      allowedHosts: "all",
      headers: {
        "Access-Control-Allow-Origin": "*",
        "Access-Control-Allow-Headers":
          "Origin, X-Requested-With, Content-Type, Accept",
      },
    },

    // INFO (serif) : The module configuration.
    module: {
      rules: [
        {
          test: /\.m?js/,
          type: "javascript/auto",
          resolve: {
            fullySpecified: false,
          },
        },
        {
          test: /\.(css|s[ac]ss)$/i,
          use: ["style-loader", "css-loader", "postcss-loader"],
        },
        {
          test: /\.(ts|tsx|js|jsx)$/,
          exclude: /node_modules/,
          use: {
            loader: "babel-loader",
          },
        },
      ],
    },

    // INFO (serif) : The plugins configuration.
    plugins: [
      new ModuleFederationPlugin({
        name: configs.appName,
        filename: configs.appFileName,
        remotes: {
          remote: configs[argv.mode].REMOTE_PATH,
        },
        exposes: {
          // INFO (serif) : Expose the components.
          "./Button": "./src/components/Button.tsx",
        },
        shared: {
          ...deps,
          react: {
            singleton: true,
            requiredVersion: deps.react,
          },
          "react-dom": {
            singleton: true,
            requiredVersion: deps["react-dom"],
          },
        },
      }),
      new HtmlWebPackPlugin({
        template: "./src/index.html",
      }),
    ],
  };
};
Enter fullscreen mode Exit fullscreen mode
  • Create Test Page in remote folder.
import React from "react";

export default function TestPage() {
  return <div className="text-blue-600 md:text-gray-600">Test Page</div>;
}
Enter fullscreen mode Exit fullscreen mode
  • Add the TestPage to remote folder webpack.config.js file. In the remote folder, webpack.config.js file should be like this:
const HtmlWebPackPlugin = require("html-webpack-plugin");
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");

// INFO (serif) : Your dependencies should be like this.
const deps = require("./package.json").dependencies;

// INFO (serif) : Your webpack config should be like this.
const configs = {
  appName: "remote",
  appFileName: "remoteEntry.js",
  development: {
    PUBLIC_PATH: "http://localhost:3001/",
    CONTAINER_PATH: "container@http://localhost:3000/remoteEntry.js",
    PORT: 3001,
  },
  production: {
    PUBLIC_PATH: "http://localhost:3001/",
    CONTAINER_PATH: "container@http://localhost:3000/remoteEntry.js",
    PORT: 3001,
  },
};

module.exports = (env, argv) => {
  console.log({ env, argv, configs: configs[argv.mode] });
  return {
    output: {
      // INFO (serif) : The output path for the build files.
      publicPath: configs[argv.mode].PUBLIC_PATH,
    },

    // INFO (serif) : The configuration for the dev server.
    resolve: {
      extensions: [".tsx", ".ts", ".jsx", ".js", ".json"],
    },

    // INFO (serif) : The configuration for the dev server.
    devServer: {
      hot: true, // INFO (serif) : Enable webpack's Hot Module Replacement feature
      port: configs[argv.mode].PORT,
      historyApiFallback: true, // INFO (serif) : When using the HTML5 History API, the index.html page will likely have to be served in place of any 404 responses.
      allowedHosts: "all", // INFO (serif) : Allow all hosts
      headers: {
        "Access-Control-Allow-Origin": "*",
        "Access-Control-Allow-Headers":
          "Origin, X-Requested-With, Content-Type, Accept",
      },
    },

    // INFO (serif) : The module configuration.
    module: {
      rules: [
        {
          test: /\.m?js/,
          type: "javascript/auto",
          resolve: {
            fullySpecified: false,
          },
        },
        {
          test: /\.(css|s[ac]ss)$/i,
          use: ["style-loader", "css-loader", "postcss-loader"],
        },
        {
          test: /\.(ts|tsx|js|jsx)$/,
          exclude: /node_modules/,
          use: {
            loader: "babel-loader",
          },
        },
      ],
    },

    // INFO (serif) : The plugins configuration.
    plugins: [
      new ModuleFederationPlugin({
        name: configs.appName,
        filename: configs.appFileName,
        remotes: {
          container: configs[argv.mode].CONTAINER_PATH,
        },
        exposes: {
          "./TestPage": "./src/pages/test/index.tsx", // INFO (serif) : your test page path
        },
        shared: {
          ...deps,
          react: {
            singleton: true,
            requiredVersion: deps.react,
          },
          "react-dom": {
            singleton: true,
            requiredVersion: deps["react-dom"],
          },
        },
      }),
      new HtmlWebPackPlugin({
        template: "./src/index.html",
      }),
    ],
  };
};
Enter fullscreen mode Exit fullscreen mode
  • Start the container and remote projects.
cd container
npm start
Enter fullscreen mode Exit fullscreen mode
cd remote
npm start
Enter fullscreen mode Exit fullscreen mode

🎉 Congratulations! Your project is ready.

Issue is How to integrate the components types to remote project?

In this section, we will integrate the components types to remote project. You can choose one of the following methods.

Method 1: Copy the types to remote project

 Method 3: Create a new package for types and publish it to npm

 Method 4: Create a local types package and integrate it to remote project

I will explain the Method 4 in this section.

  • I will use module-federation-types package for this section.

  • Install the module-federation-types package to container project.

cd container
npm i @cloudbeds/webpack-module-federation-types-plugin
Enter fullscreen mode Exit fullscreen mode
  • Create a new folder in root of container project and name it federation.config.json. It includes the exposed components types.
{
  "name": "container",
  "exposes": {
    "./Button": "./src/components/Button.tsx"
  }
}
Enter fullscreen mode Exit fullscreen mode
  • Then run the following command to generate the types.
npx make-federated-types
Enter fullscreen mode Exit fullscreen mode

If the make-federated-types command is not working and the error is like this: container/tsconfig.json: Unexpected token in JSON at position 502. You can check the tsconfig.json file and removed , from the end of the array.

✅ The tsconfig.json file should be like this:

 "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ]
Enter fullscreen mode Exit fullscreen mode

❌ Remove the , from the end of the array.

 "lib": [
      "dom",
      "dom.iterable",
      "esnext", 
    ]
Enter fullscreen mode Exit fullscreen mode
  • Create new folder for types in remote project and name it container.d.ts. Then copy the generated types to this file.
/// <reference types="react" />
declare module "container/Button" {
  import React, { ComponentProps } from "react";
  import { type VariantProps } from "class-variance-authority";
  type ButtonElementProps = ComponentProps<"button">;
  export interface ButtonProps
    extends ButtonElementProps,
      VariantProps<typeof buttonStyles> {
    label?: string;
    icon?: React.ReactNode;
    rightIcon?: React.ReactNode;
    buttonWrapperClassName?: string;
  }
  const buttonStyles: (
    props?:
      | ({
          buttonType?:
            | "error"
            | "default"
            | "success"
            | "primary"
            | "secondary"
            | "warning"
            | "info"
            | null
            | undefined;
          size?: "default" | "sm" | "lg" | "xl" | "xxl" | null | undefined;
          padding?: "default" | "sm" | "lg" | "xl" | "xxl" | null | undefined;
          rounded?:
            | "none"
            | "default"
            | "full"
            | "sm"
            | "lg"
            | "xl"
            | "xxl"
            | null
            | undefined;
          isFullWidth?: boolean | null | undefined;
        } & import("class-variance-authority/dist/types").ClassProp)
      | undefined
  ) => string;
  function Button({
    label,
    buttonType,
    rounded,
    padding,
    size,
    isFullWidth,
    className,
    ...buttonProps
  }: ButtonProps): React.JSX.Element;
  export default Button;
}
Enter fullscreen mode Exit fullscreen mode
  • 🎉 Congratulations! The types are generated. The types will be generated in types folder in dist folder. Copy the types folder to remote project.

  • Add the TestPage component type to container project. Create a new file in src folder and name it remote.d.ts. Then copy the following code to this file.

/* eslint-disable */
/// <reference types="react" />

declare module "remote/TestPage" {
  function TestPage(): JSX.Element;
  export default TestPage;
}
Enter fullscreen mode Exit fullscreen mode
  • Use the Button component in remote project. Fully types support.
// src/pages/test/index.tsx
import Button from "container/Button";
import React from "react";

export default function TestPage() {
  return (
    <div className="space-y-2 ">
      <h1 className="text-blue-600 md:text-gray-600">Test Page</h1>
      <Button label="Test Button" buttonType={"error"} />
      <Button label="Test Button" buttonType={"warning"} />
      <Button label="Test Button" buttonType={"primary"} />
      <Button label="Test Button" buttonType={"secondary"} />
      <Button label="Test Button" buttonType={"info"} />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

 Other issue with Tailwind CSS

If you are using the Tailwind CSS in your project, tailwind can generate all the container styles. But the remote project can not generate the styles. You can use the following method to solve this issue.

  • The i found the solution in safeList, you can check the Safelist documentation. You can define the Safelist in tailwind.config.js file in container project. Then you can access the styles in remote project.

❌ Don't forget to add the safeList to auto generated. It includes the only styles that you are using in your project.

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: ["./src/**/*.{js,jsx,ts,tsx}"],
  safelist: [
    // width
    { pattern: /w-\d+/, variants: ["sm", "md", "lg", "xl", "desktop"] },
    { pattern: /w-\d+\/\d+/, variants: ["sm", "md", "lg", "xl", "desktop"] },
    // height
    { pattern: /h-\d+/, variants: ["sm", "md", "lg", "xl", "desktop"] },
    { pattern: /h-\d+\/\d+/, variants: ["sm", "md", "lg", "xl", "desktop"] },
    // padding
    { pattern: /p-\d+/, variants: ["sm", "md", "lg", "xl", "desktop"] },
    { pattern: /px-\d+/, variants: ["sm", "md", "lg", "xl", "desktop"] },
    { pattern: /py-\d+/, variants: ["sm", "md", "lg", "xl", "desktop"] },
    { pattern: /pt-\d+/, variants: ["sm", "md", "lg", "xl", "desktop"] },
    { pattern: /pr-\d+/, variants: ["sm", "md", "lg", "xl", "desktop"] },
    { pattern: /pb-\d+/, variants: ["sm", "md", "lg", "xl", "desktop"] },
    { pattern: /pl-\d+/, variants: ["sm", "md", "lg", "xl", "desktop"] },
    // margin
    { pattern: /m-\d+/, variants: ["sm", "md", "lg", "xl", "desktop"] },
    { pattern: /mx-\d+/, variants: ["sm", "md", "lg", "xl", "desktop"] },
    { pattern: /my-\d+/, variants: ["sm", "md", "lg", "xl", "desktop"] },
    { pattern: /mt-\d+/, variants: ["sm", "md", "lg", "xl", "desktop"] },
    { pattern: /mr-\d+/, variants: ["sm", "md", "lg", "xl", "desktop"] },
    { pattern: /mb-\d+/, variants: ["sm", "md", "lg", "xl", "desktop"] },
    { pattern: /ml-\d+/, variants: ["sm", "md", "lg", "xl", "desktop"] },
    // gap
    { pattern: /gap-\d+/, variants: ["sm", "md", "lg", "xl", "desktop"] },
    { pattern: /gap-x-\d+/, variants: ["sm", "md", "lg", "xl", "desktop"] },
    { pattern: /gap-y-\d+/, variants: ["sm", "md", "lg", "xl", "desktop"] },
    // space
    { pattern: /space-x-\d+/, variants: ["sm", "md", "lg", "xl", "desktop"] },
    { pattern: /space-y-\d+/, variants: ["sm", "md", "lg", "xl", "desktop"] },

    // text
    { pattern: /text/ },

    // !important text
    { pattern: /!text/ },

    // background
    { pattern: /bg/ },

    // !important bg
    { pattern: /!bg/ },

    // grid & row
    { pattern: /grid/, variants: ["sm", "md", "lg", "xl", "desktop"] },
    { pattern: /grid-cols-\d+/, variants: ["sm", "md", "lg", "xl", "desktop"] },
    {
      pattern: /grid-cols-none/,
      variants: ["sm", "md", "lg", "xl", "desktop"],
    },

    // flex
    { pattern: /flex/, variants: ["sm", "md", "lg", "xl", "desktop"] },
    { pattern: /justify/, variants: ["sm", "md", "lg", "xl", "desktop"] },
    { pattern: /items/, variants: ["sm", "md", "lg", "xl", "desktop"] },

    // rounded
    { pattern: /rounded/ },
    { pattern: /rounded-none/ },
  ],
  theme: {
    extend: {},
  },
  plugins: [],
};
Enter fullscreen mode Exit fullscreen mode

Conclusion

n this guide, we've explored the creation of a micro frontend application using React and the powerful Webpack 5 Module Federation. By breaking down the process into manageable steps, we've created a container app and a remote app, showcasing how different parts can come together seamlessly.

We've covered everything from setting up the project structure, configuring Webpack, and integrating the fantastic Tailwind CSS framework. We've even tackled the issue of sharing UI components and their types between the container and remote apps.

Furthermore, by defining your components with specific types in the development environment, you can enhance the level of type safety across different repositories. TypeScript support becomes a powerful ally in ensuring that your components are used correctly and consistently, even when they're shared between different parts of your micro frontend application.

With TypeScript, you can accurately define the shape of your components, including their props and expected behavior. This approach not only helps catch potential errors during development but also provides clear documentation and guidance for other developers working with these components in remote repositories.

As a result, TypeScript support doesn't just offer a way to catch mistakes early on, but also establishes a solid foundation for collaboration and maintenance across different teams or projects. It ensures that the components' intended usage is adhered to, fostering a more robust and cohesive micro frontend architecture.

From defining UI components and their styles with Tailwind CSS to generating and sharing component types using innovative techniques, this guide has equipped you with a comprehensive understanding of building micro frontend applications.

By following these steps, you're now equipped to take on the exciting world of micro frontends and create scalable, maintainable applications that can be efficiently developed and deployed. Happy coding!

References

💖 💪 🙅 🚩
serifcolakel
Serif COLAKEL

Posted on August 13, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related