Mastering Micro Frontends: Creating Scalable Applications with React, Webpack 5, and TypeScript Type Safety
Serif COLAKEL
Posted on August 13, 2023
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
- Navigate to the project directory: Run the following command to navigate into your project directory:
cd micro-front-template
- Create a new project: Run the following command to create a new project:
npx create-mf-app container
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
- Install the packages: Run the following command to install:
npm i
- Add the tailwind: Run the following command to add the tailwind:
npm install -D tailwindcss
npx tailwindcss init
-- 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: [],
};
- Add the tailwind css file: Run the following command to add the tailwind css file:
/* ./src/index.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
- 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";
// ...
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
- Navigate to the project directory: Run the following command to navigate into your project directory:
cd micro-front-template
- Create a new project: Run the following command to create a new project:
npx create-mf-app remote
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
- Install the packages: Run the following command to install:
npm i
- Add the tailwind: Run the following command to add the tailwind:
npm install -D tailwindcss
npx tailwindcss init
-- 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: [],
};
- Add the tailwind css file: Run the following command to add the tailwind css file:
/* ./src/index.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
- 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";
// ...
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 thecontainer
&remote
folder:
cd container
npm i clsx class-variance-authority
cd remote
npm i clsx class-variance-authority
- Create a
Button.tsx
component in thesrc/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;
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",
}),
],
};
};
- 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>;
}
- Add the
TestPage
toremote
folderwebpack.config.js
file. In theremote
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",
}),
],
};
};
- Start the
container
andremote
projects.
cd container
npm start
cd remote
npm start
🎉 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 tocontainer
project.
cd container
npm i @cloudbeds/webpack-module-federation-types-plugin
- Create a new folder in root of
container
project and name itfederation.config.json
. It includes the exposed components types.
{
"name": "container",
"exposes": {
"./Button": "./src/components/Button.tsx"
}
}
- Then run the following command to generate the types.
npx make-federated-types
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 thetsconfig.json
file and removed,
from the end of the array.
✅ The tsconfig.json
file should be like this:
"lib": [
"dom",
"dom.iterable",
"esnext"
]
❌ Remove the ,
from the end of the array.
"lib": [
"dom",
"dom.iterable",
"esnext", ❌
]
- Create new folder for types in
remote
project and name itcontainer.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;
}
🎉 Congratulations! The types are generated. The types will be generated in
types
folder indist
folder. Copy thetypes
folder toremote
project.Add the
TestPage
component type tocontainer
project. Create a new file insrc
folder and name itremote.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;
}
- Use the
Button
component inremote
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>
);
}
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 theSafelist
intailwind.config.js
file incontainer
project. Then you can access the styles inremote
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: [],
};
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
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
August 13, 2023