Simple Micro Frontend Monorepo with React and Single-SPA

Agung Nugraha
9 min readApr 19, 2022

--

Today we’re going to explain how to develop simple micro frontend monorepo

agungvr — micro-frontend-monorepo
micro-frontend-monorepo

Why Micro Frontend?

The web is evolving. Applications are becoming more and more complex, and the need for more modular and maintainable code is growing with them.

Micro frontends are small web applications that are broken down into components or functions that work together to provide a larger application.

The benefits of the micro-frontend pattern include:

  • Micro-frontend architectures may be simpler, and thus easier to reason about and manage.
  • Independent development teams can collaborate on a front-end app more easily.
  • They can provide a means for migrating from an “old” app by having a “new” app running side by side with it.

Why Monorepo?

  • Easier to manage, since you don’t need to worry about updating every other repo when changing one’s version
  • Sharing code is easier, we can add a project as another project’s dependency
  • Sharing the same node_modules for dependencies will save you a little space on your computer

Let’s Begin

Before we discuss the step-by-step instructions, let’s get a quick overview of what makes up the demo app. This app is composed of three sub-apps:

Setting up project

First, you need to have lerna installed globally:

yarn global add lerna

Create a new repository:

git init microfrontend-monorepo && cd microfrontend-monorepo

And initialise lerna inside the repo:

lerna init

Once you did it, your folder should look like this:

agungvr — micro-frontend-monorepo
The packages folder is where we’ll put our applications.

We are using Yarn and Yarn workspaces. Yarn workspaces will make our life easier by linking projects and allowing them to consume each other living in the same repository. For more info about Yarn workspaces check the docs https://yarnpkg.com/features/workspaces

We need to tell Lerna we’re using Yarn with workspaces, so add these two lines to lerna.json:

{
"packages": [
"packages/*"
],
"version": "0.0.0",
+ "npmClient": "yarn",
+ "useWorkspaces": true
}

You also need to tell package.json the folders we’re gonna take as workspaces by adding the following:

{
"name": "root",
"private": true,
"devDependencies": {
"lerna": "^4.0.0"
},
+ "workspaces": [
+ "packages/*"
+ ]
}

Creating Root Config

To create a microfrontend app for this demo, we’re going to use a command-line interface (CLI) tool called create-single-spa.

Single-SPA — Framework for bringing together multiple JavaScript microfontends in a frontend application

use the command:

npx create-single-spa

And answer it like the following:

agungvr — micro-frontend-monorepo
single-spa root config — App that contains the HTML (EJS) shared between the whole application and register apps. Is the one that orchestrates the micro frontend.

Next we’ll add a version to the /micro-frontend-container/package.json:

{
"name": "@vr/root-config",
+ "version": "1.0.0",
"scripts": {
...

Add your organization name to the root’s package.json file and add the following scripts:

{
- "name": "root",
+ "name": "@vr/root",
"private": true,
+ "scripts": {
+ "bootstrap": "lerna bootstrap",
+ "start": "lerna run start --stream"
},
...

Run the bootstrap command to fetch packages and link dependencies. If the node_modules of your projects ever have packages installed, just run this command again in the root directory.

yarn bootstrap

The lerna run command chooses the command that the packages will receive to run. As the command to run the project in the micro-frontend-container/package.json is "start", we put lerna run start. Adding --stream allow us to see the output of our different packages following its names.

yarn start
agungvr — micro-frontend-monorepo

Great!, you’ll see default ui from single-spa root config. Ok stop running the server and let’s keep going

Creating the Micro-Frontend Apps

Let’s, again, from the root directory, run:

npx create-single-spa

This time our answers will be:

agungvr — micro-frontend-monorepo

Next add version & modify the start script of the new package.json to include the port:

{
"name": "@vr/app1",
+ "version": "1.0.0",
"scripts": {
- "start": "webpack serve",
+ "start": "webpack serve --port 9001",
...

Now we have to inform our microfrontend-container that it should run this project. For that, we’ll add this line to the packages/micro-frontend-container/src/index.ejs:

<% if (isLocal) { %>
<script type="systemjs-importmap">
{
"imports": {
"@vr/root-config": "//localhost:9000/vr-root-config.js",
+ "@vr/app1": "//localhost:9001/vr-app1.js"
}
}
</script>
<% } %>

Next modify in `microfrontend-layout.html` change the application to our apps name

<single-spa-router>
<main>
<route default>
- <application name="@single-spa/welcome"></application>
+ <application name="@vr/app1"></application>
</route>
</main>
</single-spa-router>

Ok, let’s try:

yarn start
agungvr — micro-frontend-monorepo
micro-frontend-app1 is mounted

if you did everything right, you should be able to see your default page from app1!

Create simple UI with router

We’ll create a folder with the following files:

agungvr — micro-frontend-monorepo

first we need to install react-router-dom for a specific package => micro-frontend-app1

lerna add react-router-dom --scope=@vr/app1
  • /micro-frontend-app1/src/components/Card/index.tsx
import "./card.css";const Card = () => (
<div className="card1">
<h3>Card From App1</h3>
</div>
);
export default Card;
  • /micro-frontend-app1/src/components/Card/card.css
.card1 {
width: 400px;
background: #39c999;
border-radius: 12px;
margin: 0 12px 24px;
}
  • /micro-frontend-app1/src/pages/about/index.tsx
function About() {
return <h2>App 1 - About Page</h2>;
}
export default About;
  • /micro-frontend-app1/src/pages/home/index.tsx
import { Link } from "react-router-dom";
import logo from "./logo.svg";
import CardApp1 from "../../components/Card";
import "./home.css";
function Home() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>Micro Frontend Monorepo</p>
<div className="card-wrapper">
<CardApp1 />
</div>
<div className="mb12">
<Link to="/about">Go to App 1 - About</Link>
</div>
</header>
</div>
);
}
export default Home;
  • /micro-frontend-app1/src/pages/home/home.css
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace;
}
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
.mb12 {
margin-bottom: 12px;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
a {
text-decoration: none;
color: white;
font-size: 18px;
}
a:hover {
color: gray;
}
.card-wrapper {
display: flex;
}
  • /micro-frontend-app1/src/root.component.tsx
import { lazy, Suspense } from "react";
import { BrowserRouter, Routes, Route } from "react-router-dom";
const HomePage = lazy(() => import("./pages/home"));
const AboutPage = lazy(() => import("./pages/about"));
function Root() {
return (
<BrowserRouter>
<Suspense fallback="">
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/about" element={<AboutPage />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}
export default Root;

Ok, let’s try

yarn start
agungvr — micro-frontend-monorepo

if you try to open http://localhost:9001 you will see:

agungvr — micro-frontend-monorepo

Cool! Let’s, create another micro-frontend app with the same flow:

npx create-single-spa 
  • /micro-frontend-container/src/index.ejs
<% if (isLocal) { %>
<script type="systemjs-importmap">
{
"imports": {
"@vr/root-config": "//localhost:9000/vr-root-config.js",
"@vr/app1": "//localhost:9001/vr-app1.js",
+ "@vr/app2": "//localhost:9001/vr-app2.js"
}
}
</script>
<% } %>
  • /micro-frontend-app2/package.json
{
"name": "@vr/app2",
+ "version": "1.0.0",
"scripts": {
- "start": "webpack serve",
+ "start": "webpack serve --port 9002",
...
  • /micro-frontend-app2/src/components/Card/index.tsx
import "./card.css";const Card = () => (
<div className="card1">
<h3>Card From App1</h3>
</div>
);
export default Card;
  • /micro-frontend-app2/src/components/Card/card.css
.card1 {
width: 400px;
background: #39c999;
border-radius: 12px;
margin: 0 12px 24px;
}
  • /micro-frontend-app2/src/pages/home/index.tsx
const Home = () => {
return <h2>App 2 - Home Page</h2>;
};
export default Home;
  • /micro-frontend-app2/src/root.component.tsx
import HomePage from "./pages/home";const App = () => {
return <HomePage />;
};
export default App;

add route in `micro-frontend-app1` at home pages

  • /micro-frontend-app1/src/pages/home/index.tsx
import { Link } from "react-router-dom";
import logo from "./logo.svg";
import CardApp1 from "../../components/Card";
import "./home.css";
function Home() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>Micro Frontend Monorepo</p>
<div className="card-wrapper">
<CardApp1 />
</div>
<div className="mb12">
<Link to="/about">Go to App 1 - About</Link>
</div>
+ <div className="mb12">
+ <Link to="/app2">Go to App 2 - Home</Link>
+ </div>
</header>
</div>
);
}
export default Home;
  • /microfrontend-container/src/microfrontend-layout.html
<single-spa-router>
<main>
+ <route path="app2>
+ <application name="@vr/app1"></application>
+ </route>
<route default>
<application name="@vr/app1"></application>
</route>
</main>
</single-spa-router>

The repo will like this:

agungvr — micro-frontend-monorepo

Now we need to link dependencies node_modules with run command:

yarn bootstrap
yarn start
agungvr — micro-frontend-monorepo

Now if we click `Go to App 2- Home` it will navigate to /app2

Looks great right? 😎

Now how to shared component from microfrontend app to another microfrontend app?

We will share card component from micro-frontend-app2 to micro-frontend-app1

export your card component in vr-app2.tsx

  • /micro-frontend-app2/src/vr-app2.tsx
...
export const { bootstrap, mount, unmount } = lifecycles;
+ export { * as Card } from "./components/Card/index";

Now before we export the card component, we need to have function to handle async import like this:

  • create utils directory and importModule file in micro-frontend-app1
import React, { lazy } from "react";const handleImport = async (appName, moduleName) => {try {
const comp = await System.import(appName);
return comp[moduleName];
} catch {
return () => <React.Fragment />;
}
};
export const importModule = (appName, moduleName) =>
lazy(() =>
handleImport(appName, moduleName).then((comp) => ({
default: comp,
}))
);

Now we can import in home page

  • /micro-frontend-app1/src/pages/home/index.tsx
import { Link } from "react-router-dom";
import logo from "./logo.svg";
import CardApp1 from "../../components/Card";
import "./home.css";
+ import { importModule } from "../../utils/importModule";+ const CardApp2 = importModule("@vr/app2", "Card");function Home() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>Micro Frontend Monorepo</p>
<div className="card-wrapper">
<CardApp1 />
+ <CardApp2 />
</div>
<div className="mb12">
<Link to="/about">Go to App 1 - About</Link>
</div>
<div className="mb12">
<Link to="/app2">Go to App 2 - Home</Link>
</div>
</header>
</div>
);
}
export default Home;

Ok let’s try

agungvr — micro-frontend-monorepo

Cool. That looks and works much better! 🥳🥳

Now I will show you this app in the server

when server micro-frontend-app2 died:

Great! this app still ALIVE 🥳🥳🥳🥳🥳🥳🥳🥳

Conclusion

Micro-frontends are the future of frontend web development.

The benefits are massive, including independent deployments, independent areas of ownership, faster build and test times, and the ability to mix and match various frameworks if needed.

Full source on my github

--

--