You most probably have worked or will be working on several react projects. After a certain point, you will notice, there are several components which are written in many projects e.g Button component.
Have you wondered how to reuse or share the component across the projects so that we don’t have to duplicate it? Yes, we are going to talk about sharing the component.
There are mainly 2 ways to share the component –
1. Build time – extract the component and build it as npm package which can be installed at build time in multiple apps.
2. Run time – host application loads the shared component from other apps through network request and use it with lazy loading.
Run time sharing was not possible before due to several complications, but Module federation from webpack solves them all. It is a hot topic right now. So we are going to deep dive into module federation in this article.
What is Module Federation?
Zackary Jackson created the module federation which is now officially part of webpack. It is available in webpack version 5 and higher.
Module federation allows a JavaScript application to dynamically load code from another application — in the process, sharing dependencies.
Zackary Jackson
In a nutshell, Module federation is a technique trying to achieve code sharing between javascript applications at runtime.
Let me clear one more thing – Module Federation is not a framework, it is a webpack plugin that allows us to share the dependencies and code, independent of the framework application is using.
So, in this article, we will be using React and share the React component using Module Federation but the same can be applied for Vue.js or Angular.js.
Key terms in Module Federation
There are few terms which will be keep repeating throughout the article so let’s write down that here –
Host
The Host is the application that will load the remote application component.
Remote
Remote is the application that shares the react component and dependencies through the webpack 5 module federation plugin.
Building a Federated Application
Let us create a folder – module-federation. This is optional but recommended as it keeps things together.
mkdir module-federation
cd module-federation
Now, inside the module-federation folder, create 2 react projects host and remote. In this, host app will run on port 3000 and the remote app will run on port 4000 of localhost.
npx create-react-app host
npx create-react-app remote
Install Dependencies
The above command will have created 2 folders –
- /host
- /remote
Now, within each folder install the following package dependencies –
npm install --save-dev webpack webpack-cli html-webpack-plugin webpack-dev-server babel-loader css-loader
Host application setup
We will start setting up the host application first and then remote apps later.
This host application will be running on localhost:3000 and consume the remote shared component.
In Host application setup we will do the following –
- Creation of webpack.config.js
- Change in index.js file
- Creation of bootstrap.js file
- Creation of ErrorBoundary component
- Changes in App.js file
- Changes in package.json file
Let us create the webpack.config.js file in the root of the host app.
1. Creation of webpack.config.js
Basic configuration –
// filepath: host/src/webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: "./src/index.js",
mode: "development",
devServer: {
port: 3000
},
module: {
rules: [
{
test: /\.(js|jsx)?$/,
exclude: /node_modules/,
use: [
{
loader: "babel-loader",
options: {
presets: ["@babel/preset-env", "@babel/preset-react"]
}
}
]
},
{
test: /\.(css|s[ac]ss)$/i,
use: ["style-loader", "css-loader"]
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: "./public/index.html"
})
],
resolve: {
extensions: [".js", ".jsx"],
},
target: "web"
};
You can see, we are exposing port 3000 in development mode. We are extracting all the js, and jsx files (except node_modules folder files) and passing them to Babel for transpiling them.
Similarly, styling files i.e css, sass, and scss files are extracted and passed to the css-loader first, and then the result is passed to the style-loader for further transpilation.
After transpilation is done, the code is injected into the index.html file.
Let’s add the module federation plugin –
// filepath: host/src/webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
const { dependencies } = require('./package.json');
module.exports = {
entry: "./src/index.js",
mode: "development",
devServer: {
port: 3000
},
module: {
rules: [
{
test: /\.(js|jsx)?$/,
exclude: /node_modules/,
use: [
{
loader: "babel-loader",
options: {
presets: ["@babel/preset-env", "@babel/preset-react"]
}
}
]
},
{
test: /\.(css|s[ac]ss)$/i,
use: ["style-loader", "css-loader"]
}
]
},
plugins: [
new ModuleFederationPlugin({
name: "Host",
remotes: {
Remote: `Remote@http://localhost:4000/remoteEntry.js`
},
shared: {
...dependencies,
react: {
singleton: true,
requiredVersion: dependencies["react"]
},
"react-dom": {
singleton: true,
requiredVersion: dependencies["react-dom"]
},
}
}),
new HtmlWebpackPlugin({
template: "./public/index.html"
})
],
resolve: {
extensions: [".js", ".jsx"],
},
target: "web"
};
The text in yellow color are the changes from the basic configuration.
Before, we go further, let’s understand the changes –
- Load the ModuleFederationPlugin from the webpack lib
- Loading the dependencies which will be shared through the plugin
- name: This is used to tell the name of the application/module. This is not important for the Host as it is not exposing anything but it is a must for the remote apps.
- remotes: This keeps a journal of all federated modules. It will be an object where Keys are the internal name of the module and its value is the location of the module. e.g. Remote is the internal name to access the shared component of the remote app. The name can be anything but it should be unique among the Host and Remotes.
- Location – Remote@http://localhost:4000/remoteEntry.js –
-
- Remote is the name field of the Remote app. You guessed correctly that the internal name and the remote name could be different. The internal name is used to access the remote module inside the host app.
- The remote module is hosted on localhost:4000
- The shared component from the remote module is accessible from the remoteEntry.js file.
- Remote is the name field of the Remote app. You guessed correctly that the internal name and the remote name could be different. The internal name is used to access the remote module inside the host app.
-
We are done with the webpack configuration. Let’s work on the other files to complete the setup on host application.
2. Change in index.js file
Since host app is using the remote app, we want remote application to be loaded as soon as possible. To make this work, we will use index.js to import the bootstrap.js file.
So index.js will be just a plain entry point and responsibility of rendering the react app lies with bootstrap.js (you can have any name) file.
// filepath: host/src/index.js
import('./bootstrap');
The reason for doing this internal redirection is to give webpack a chance to load all the imports required for rendering the remote app. And if we don’t do the above will get the error – Shared module is not available for eager consumption
3. Creation of bootstrap.js file
// filepath: host/src/bootstrap.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
4. Creation of ErrorBoundary component
The host app will make a network request for the shared component of remote app. As this can give a runtime error e.g. script loading failure, timeout error, etc, so we need to handle these.
React offers ErrorBoundary component to handle the runtime errors.
An ErrorBoundary is a special component that handles runtime errors. Any component can be an ErrorBoundary component if it follows the below 2 constraints –
React official docs
- It should be a class component
- It must implement either
getDerivedStateFromError
orcomponentDidCatch
We can implement the ErrorBoundary on our own or use a npm package react-error-boundary
Let’s create a component folder inside /src folder and create an ErrorBoundary component –
// filepath: host/src/component/ErrorBoundary.js
import React from "react";
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// Update state so the next render will show the fallback UI.
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// You can also log the error to an error reporting service
// logErrorToMyService(error, errorInfo);
console.log(error, errorInfo);
}
render() {
if (this.state.hasError) {
return <h1>Something went wrong.</h1>
}
return this.props.children;
}
}
export default ErrorBoundary;
Let’s create another component RemoteComponentWrapper inside the component folder. This will handle the runtime errors and show “Something went wrong.” if any error occurs.
// filepath: host/src/component/RemoteComponentWrapper.js
import React from 'react';
import ErrorBoundary from "./ErrorBoundary";
const RemoteComponentWrapper = ({ children }) => {
return (
<div className='remote'>
<ErrorBoundary>{children}</ErrorBoundary>
</div>
);
};
export default RemoteComponentWrapper;
5. Changes in App.js file
// filepath: host/src/App.js
import React from 'react';
import RemoteComponentWrapper from './component/RemoteComponentWrapper';
import './App.css';
// Lazy loading the header and footer component from Remote app
const Header = React.lazy(() => import("Remote/Header"));
const Footer = React.lazy(() => import("Remote/Footer"));
function App() {
return (
<div className="App">
<RemoteComponentWrapper>
<Header />
</RemoteComponentWrapper>
<p>This is from host</p>
<RemoteComponentWrapper>
<Footer />
</RemoteComponentWrapper>
</div>
);
}
export default App;
6. Change in the package.json file
The setup of the host application is almost complete.
Let’s modify the package.json file to use the webpack config for running the app.
"scripts":{
"start": "webpack serve"
}
Now, running npm start command will start the host application at port 3000
Since shared component Header and Footer from remote app is not available yet, so Something went wrong. is displayed in place of those.
This is the expected behaviour, the host app is trying to download the localhost:4000/remoteEntry.js file which is not available yet.
Let’s build the remote app and expose the Header and Footer components that will fix the above error.
Remote app setup
This app will act like a provider i.e it will share the components that host applications will consume.
1. Creation of webpack.config.js file
Let’s create the webpack.config.js file in the root of the remote app. This will be mostly the same as the host’s webpack file. The difference is highlighted in yellow.
// filepath: remote/webpack.config.js
const HtmlWebpackPlugin = require("html-webpack-plugin");
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
const path = require("path");
const { dependencies } = require("./package.json");
module.exports = {
entry: "./src/index",
mode: "development",
filename: "remoteEntry.js",
devServer: {
port: 4000,
},
module: {
rules: [
{
test: /\.(js|jsx)?$/,
exclude: /node_modules/,
use: [
{
loader: "babel-loader",
options: {
presets: ["@babel/preset-env", "@babel/preset-react"],
},
},
],
},
{
test: /\.(css|s[ac]ss)$/i,
use: ["style-loader", "css-loader"],
},
],
},
plugins: [
new ModuleFederationPlugin({
name: "Remote",
filename: "remoteEntry.js",
exposes: {
"./Header": "./src/component/Header.jsx",
"./Footer": "./src/component/Footer.jsx"
},
shared: {
...dependencies,
react: {
singleton: true,
requiredVersion: dependencies["react"],
},
"react-dom": {
singleton: true,
requiredVersion: dependencies["react-dom"],
},
},
}),
new HtmlWebpackPlugin({
template: "./dist/index.html",
}),
],
resolve: {
extensions: [".js", ".jsx"],
},
target: "web",
};
The top 3 things to note here are –
- name: this is the name of the remote application. The host application will use this name to access the remotely shared component.
- exposes: contains the components list which will be accessible by the host applications.
- filename: the host application will download this file over a network request. It contains the shareable components references.
2. Change in index.js file
Exactly same as Host application setup (ref point 2. of host app setup)
3. Creation of bootstrap.js file
Exactly same as Host application setup (ref point 3. of host app setup)
4. Change in package.json file
Exactly same as Host application setup (ref point 6. of host app setup)
5. Creation of shared components
Header.jsx
// filepath: remote/src/component/Header.jsx
import React from 'react'
import './header.css';
const Header = () => {
return (
<div className='header'>Header from Remote</div>
)
}
export default Header;
Footer.jsx
// filepath: remote/src/component/Footer.jsx
import React from 'react'
import './header.css';
const Footer = () => {
return (
<div className='footer'>Footer from remote</div>
)
}
export default Footer;
header.css
// filepath: remote/src/component/header.css
.header, .footer {
color: white;
background-color: red;
min-height: 60px;
display: flex;
align-items: center;
justify-content: center;
}
Just to see if we are loading which Remote app, I’ve written a heading in App.js
App.js
// filepath: remote/src/App.js
import React from 'react';
import './App.css';
function App() {
return (
<div className="App">
<h1>This is Remote app</h1>
</div>
);
}
export default App;
Now run the npm start command.
It will open the remote app on localhost:4000
And reloading the host app i.e. localhost:3000 should be able to load the Header and Footer component from the remote app.
Conclusion
Now we know how to share the components across projects.
You can see that we are able to use the shared components of the remote application. And this is not limited to one remote app, i.e. the host app will be able to use the shared components from n number of remote apps.
For simplicity, we have used host and remote but they can be mixed together i.e. host can also share the component which can be used by others so sharing is bi-directional here.
I hope you’ve learned something from this article, please share it among your friends.
If you’re interested in react.js then please check out react.js projects
Thanks for now, will meet you soon with another article!