tldr; Here is the github repo for this tutorial.
Introduction
You are working on a react project, but for whatever reason you opened the components directory and realized that you have hundreds of components that some are related to each other and some are not, do you think you can better?
ofc, you can!
I here want to make a quick guide into project setup using Webpack, in the future I will do a NextTS
instead of CRA
.
When to use this guide?
You have a
take home project
in your interviews for a senior role that has multiple domains, for example E-commerce with cart and items page, and you want to show you can split the app and split their responsibilities each for an independent team.You learning Lerna, Module federations and the Monorepo world, and you want to have a practical experience with having an up and running environment.
Before we jump in the code let's just look at our toolkit;
What is a Monorepo?
Based on the definition from the official site
A Monorepo is a single repository containing multiple distinct projects, with well-defined relationships.
You can think of your monolith repo as a sub project each has a team, and domain, this team takes ownership for developing, testing & deploying this Monorepo, this structure aims to help mananging the project efficiently.
What is lerna?
Lerna is the Monorepo tool for typescript, it helps managing different projects and run parallel, and since v6
they are utilizing Nx caching and command distribution which you can check my series of blogs about Nx
What is Module Federation?
Question, have you imported modules dynamically?
Why are not you just import them right away?
dynamic import helps in improving FCP, since the browser does it wait all content to be loaded to load the page.
But Module Federation takes anther step, and helps you to import the modules on run time, meaning you can deploy the federated modules independently. This helps more towards teams autonomy.
Set up Environment!
First we need to create the directory for the project:
mkdir ./monorepo-tutorial
cd ./monorepo-tutorial
npx lerna init
mkdir ./apps
mkdir ./libs
Let's take a moment to understand what we have created.
Firstly, we need to look at our files created
├── apps
├── libs
├── package.json
├── .gitignore
├── lerna.json
One thing in particular interesting in mentioning is workspaces
inside the package.json
.
Workspaces is a generic term that refers to the set of features in the npm cli that provides support to managing multiple packages from your local files system from within a singular top-level, root package.
Since we created /apps
we need to go to package.json
and use apps
instead of packages
as shown below:
{
"name": "root",
"private": true,
"workspaces": [
"apps/*"
],
"dependencies": {},
"devDependencies": {
"lerna": "^7.2.0"
}
}
Now we install the global dependancies we need
npm i -D @swc/helpers @swc/core @swc/cli
Personally I see what makes lerna special is that if you are aware of package.json and setting up env with your favorite tools there is no extra setups nor learning curve.
Now let's create react apps, we will have two apps, one called app1
where it will contain the components. and a host that will connect to our remote app aka app1
and lazily imports the module to be consumed inside host
.
You can have as much remotes as you want.
cd ./apps
npx create-react-app app1 --template typescript
npx create-react-app host --template typescript
Host Set up
Now let's enter
cd ./host
The great thing about
lerna
is you can setup each app the way it fits your needs.
first we install dependancies
npm i -D webpack-merge webpack-dev-server webpack-cli webpack ts-loader npm-run-all html-webpack-plugin @module-federation/typescript
Now let's take a moment to understand what we are installing here:
- webpack-dev-server: in order to run the dev-server you need to set up the devServer object in your webpack config.
webpack-dev-server v4.0.0+ requires node >= v12.13.0, webpack >= v4.37.0 (but we recommend using webpack >= v5.0.0), and webpack-cli >= v4.7.0.
webpack-merge: Will help in setting up a
base
and override it withdev
andprod
configs, this approach will in maintaining consistency in the bundling configuration.webpack-cli: it takes where the configs are located and run them.
ts-loader: a webpack overriding rule that takes typescript and bundles it in the webpack, to be consumed in the browser -in our case I mean-.
html-webpack-plugin: this plugin will create the script tag inside the public/index.html file. It is essential since we are using SPA.
@module-federation/typescript: Module federation
v1
still not supporting typescript by nature, but it is a planned feature forv2
as mentioned by the creator of module-federation.
second we setup webpack
mkdir ./configs
touch federationConfig.js
touch webpack.base.js
touch webpack.dev.js
touch webpack.prod.js
with the following code from here
I will explain the module federation config in depth in the future.
For now we need to go to package.json
and set up the following scripts
"scripts": {
"start": "webpack --watch --config configs/webpack.dev.js",
"build": "webpack --progress --config configs/webpack.prod.js",
"serve": "webpack serve --config configs/webpack.dev.js",
"dev": "npm-run-all --parallel start serve",
"tscheck": "tsc"
}
finally we set up the src
we go to /src
and delete all of the files, create the following ones:
// apps/host/src/App.tsx
import React from "react";
const App1 = React.lazy(() => import("app1/App"));
function App() {
return (
<>
<App1 />
<div className="App">host</div>
</>
);
}
export default App;
Since we are importing a module from another federated remote, the ts
compiler will throw an error since it can't find app1/App
module, to solve this issue add the following:
// apps/host/src/react-app-env.d.ts
/// <reference types="react-scripts" />
declare module "app1/App";
Now we need to render the app we created above.
// apps/host/src/bootstrap.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement
);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
We now need to create index file for the src directory:
// apps/host/src/index.ts
import("./bootstrap");
export {};
Now we need to do the same steps we did above for all of the remote app1
but with few changes on the federation config can be found here:
const dependencies = require("../package.json").dependencies;
module.exports = {
name: "app1",
filename: "remoteEntry.js",
exposes: {
"./App": "./src/App",
},
shared: {
...dependencies,
react: {
singleton: true,
requiredVersion: dependencies["react"],
},
"react-dom": {
singleton: true,
requiredVersion: dependencies["react-dom"],
},
},
};
Now all set up and running!
Top comments (1)
Turborepo (monorepo) might be the proper answer over module federation, if code sharing is the main concern but building the whole repository is a deal breaker.
I could see module federation as a way to split the frontend between teams that communicate with contracts, in a way similar to microservices.
If the frontend team is relatively small or highly communicative, just go with a monorepo! Plus we could share types/validations between the backend and frontend more easily than packagings. wink, wink
Great demo on the module federation! I had a attempt at it using nuxtjs before but ended up working with a monorepo too.