Micro Frontends Architecture with Webpack Module Federation (Part 1)
We have an Admin Panel written in React where we track and manage all the stages of orders in Trendyol GO (Hızlı Market and Trendyol Yemek). A monolithic structure met our expectations very well since we were the only team at first and the number of employees was small.
Monolithic structures are not always bad. If it meets your expectations and if it doesn’t give you a negative effect then you can use every method. The important thing is; finding the way to reach your goal in the most efficient way.
After about 1.5 years, we divided our teams into meaningful domains with the philosophy of Domain Driven Design (DDD) when the number of our teammates became sufficient. At this point, we had to design the Micro Frontends structure so that each team could move freely and feel as independent as possible.
I had Micro Frontends experience in a few different projects before. I also designed a Micro Frontends architecture myself but it was more logical to researching all other alternatives and choosing the one that would best meet our needs. We explored all the alternatives and weighed the pros and cons of each (I’m not going to talk about all the alternatives in this article because that’s a different topic.) and at the end of our evaluation, we decided that Webpack Module Federation would meet our needs very well.
Why Webpack Module Federation?
When we examined all the alternatives, it made much more sense to prefer Webpack Module Federation for the following reasons.
- No maintenance costs (There will be maintenance costs if you build an architecture yourself)
- Architecture is not fragile (It can be fragile if you build an architecture yourself)
- No team specific learning costs (There will be learning costs if you build an architecture yourself)
- Transition to Module Federation is very fast
- No need to re-architect every project
- All requirements are taken care of at the build time
- No extra work needed in the runtime
- You can share dependencies very easily
- Library/Framework independent
- You are not dealing with all the compression and cache issues
- You are not dealing with routing issues
- Shell and Micro Apps are not tightly coupled but loosely coupled
How to use Webpack Module Federation?
You can use it in 3 different ways.
1. Domain
In this way, you can create as many Micro Frontends (Apps) as you want and manage completely independent domains with Shell App. For example, imagine that there is a Menu in Shell App and when the links are clicked, it will bring up the relevant Apps on the right.
2. Widget
In this way, you can add any widget/component (i.e. a small piece of code) from any App to any App. You can expose UserDetail component in User App in Product App.
3. Hybrid
You can use the 1st and 2nd ways together.
Talk is cheap, show me the code!
Let’s make a PoC (Proof of Concept) right away and see why it is so much better than other methods.
I added this PoC to the Github organization called https://github.com/module-federation-examples, you can fork directly and try as many times as you want :)
First, let’s create our Shell App. This will be our manager App. I will create all Apps with the create-react-app script because it is easier. If you want, you can install it normally. Although a ready-made Webpack config file will be added to the repo with create-react-app, we will modify this file in the next steps.
Not: create-react-app doesn’t support Webpack 5 yet so we will add it manually.
npx create-react-app shellcd shellyarn add webpack webpack-cli webpack-server html-webpack-plugin css-loader style-loader babel-loader webpack-dev-server
We create our Product and User Apps in the same way as above, only by changing the names.
Step 1 — Bootstraping
Let’s add a file named bootstrap.js under the src folder and its content will be as follows;
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';ReactDOM.render(
<App />,
document.getElementById('root')
);
Let’s change the content of the index.js file under the src folder as follows.
import('./bootstrap');
We do these operations so that we can make asynchronous installation, that is, to be sure that the Apps are completely ready. Let’s repeat these operations in all 3 repos.
Step 2 — Webpack Config
We add a file named webpack.config.js to the root of the Shell App and change its content as follows.
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
const deps = require('./package.json').dependencies;module.exports = {
mode: 'development',
devServer: {
port: 3001,
},
module: {
rules: [
{
test: /\.js?$/,
exclude: /node_modules/,
loader: 'babel-loader',
options: {
presets: [
'@babel/preset-env',
'@babel/preset-react',
],
},
},
{
test: /\.css$/i,
use: ["style-loader", "css-loader"],
},
],
},
plugins: [
new ModuleFederationPlugin(
{
name: 'SHELL',
filename: 'remoteEntry.js',
shared: [
{
...deps,
react: { requiredVersion: deps.react, singleton: true },
'react-dom': {
requiredVersion: deps['react-dom'],
singleton: true,
},
},
],
}
),
new HtmlWebpackPlugin({
template:
'./public/index.html',
}),
],
};
The only part in this file that is related to the Micro Frontends is the ModuleFederationPlugin function and the object we send into it. Other lines are about normal Webpack config.
- name: We use it to determine the name of the application. We will communicate with other Apps through this name.
- filename: We use it as an entry file. In this example, other Apps will be able to access SHELL App by typing “SHELL@http://localhost:3001/remoteEntry.js”.
- shared: We use it to specify which dependencies this application will share with other applications. The point to note here is “singleton: true”. If you don’t write “singleton: true”, each app will run on a separate React Instance.
Copy the same file to Product and User App, but don’t forget to increase the port and change the name field.
Step 3 — Design
Let’s change the App.js under the src folder as follows.
import React from 'react';
import './App.css';const App = () => (
<div className="shell-app">
<h2>Hi from Shell App</h2>
</div>
);export default App;
Let’s change the App.css under the src folder as follows.
.shell-app {
margin: 5px;
text-align: center;
background: #FFF3E0;
border: 1px dashed #FFB74D;
border-radius: 5px;
color: #FFB74D;
}
Let’s make the same change in Product and User Apps and change the places that say shell. Don’t forget to change the colors too.
Now run the following command by entering each repo one by one.
yarn webpack server
Now all our applications are ready for Micro Frontends architecture and can run independently of each other. 🎉
Step 4 — Final Touches
It’s time to mention 2 great features in Module Federation :)
- exposes: It allows you to share a component, a page or an entire application from any application to another. Everything you expose is created as a separate build, thus creating a natural tree shaking. Each build is named with the file’s MD5 hash so you don’t have to worry about caching.
- remotes: It determines from which applications you will receive a component, a page, or the application itself.
Each application can both expose and define a remote and do them multiple times.
Now let’s expose the Product App to the Shell App and let’s make our first Micro Frontends connection.
Let’s open the webpack.config.js file in the product repo and change the object sent to the ModuleFederationPlugin method in it as follows. The value in the exposes object determines which component it shares in the repo, and the key in the object determines what name other applications can access this component.
new ModuleFederationPlugin(
{
name: 'PRODUCT',
filename: 'remoteEntry.js',
exposes: {
'./App': './src/App',
},
shared: [
{
...deps,
react: { requiredVersion: deps.react, singleton: true },
'react-dom': {
requiredVersion: deps['react-dom'],
singleton: true,
},
},
],
}
),
Let’s open the webpack.config.js file in the shell repo and change the object sent to the ModuleFederationPlugin method in it as follows. The value in the remotes object determines how to access the product repository (the name before the @ sign must be the same as the name in the Webpack config in the product repository), the key in the object allows us to access the product repository with just the name.
new ModuleFederationPlugin(
{
name: 'SHELL',
filename: 'remoteEntry.js',
remotes: {
PRODUCT: 'PRODUCT@http://localhost:3002/remoteEntry.js'
},
shared: [
{
...deps,
react: { requiredVersion: deps.react, singleton: true },
'react-dom': {
requiredVersion: deps['react-dom'],
singleton: true,
},
},
],
}
),
We have linked 2 Apps together. Now let’s see how to use Product App in Shell App. Let’s open App.js in the shell repository and make the following changes.
import React from 'react';
import './App.css';const ProductApp = React.lazy(
() => import('PRODUCT/App')
);const App = () => (
<div className="App">
<h2>Hi from Shell App</h2><React.Suspense fallback='Loading...'>
<ProductApp />
</React.Suspense>
</div>
);export default App;
We have defined the component named “App”, which is exposed from the PRODUCT App with the lazy method of React, to our variable named ProductApp. We need to use the lazy function for the components we will get from a different Micro Frontends, and we need to use Suspense to use it in the template part so that we can make sure that everything is loaded on the page.
Yeah, that’s it 🎉
If you want now you can add a component to User App and try to use this component in Product App :) You can share as many components as you want using expose and remotes. Zack Jackson who created Module Federation has a Github repo called module-federation-examples. In this repo, there are sample applications on many subjects such as React, Vue, Angular, Server Side Rendering, Shared Routing, you can review them if you want.
Conclusion
Webpack Module Federation isn’t fully stable yet, but it’s still the best solution. We are currently using this method for Trendyol GO in Live, 1 different team is using it live in Trendyol and 2 different teams have decided to switch to Webpack Module Federation. Module Federation is so fast and makes things so easy, no need to reinvent the wheel :) I’m sure it will be much better in future versions of Webpack :)
In our article named Micro Frontends Architecture with Webpack Module Federation (Part 2), we tell about our experiences from PoC to Live. Enjoy it :)
See you in our new articles.