Setting up remote components and services with webpack module federation
Motivation
In a multi-team environment, all teams should be able to develop and deploy independently and still be deploy a singular application. Another reason to implement this is to be able to create shared remote modules which are extensible and deployed separately, compared to npm package approach where all the application needs to rebuild again to deploy the changes.
Setting up Federated remote application
- Install webpack 5 in your application
npm i -D webpack webpack-cli webpack-dev-server
2. Create a webpack.config.js file
const path = require("path");
const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const ModuleFederationPlugin = require("webpack").container.ModuleFederationPlugin;
module.exports = {
// Where files should be sent once they are bundled
entry: './src/index.ts',
output: {
publicPath: '/remote',
path: path.join(__dirname, '/dist'),
filename: 'index.[hash].js',
},
// webpack 5 comes with devServer which loads in development mode
devServer: {
host: '0.0.0.0',
port: 3000,
open: true,
},
resolve: {
extensions: ['.js', '.jsx', '.ts', '.tsx']
},
// Rules of how webpack will take our files, complie & bundle them for the browser
module: {
rules: [
{
test: /\.(ts|tsx)?$/,
loader: 'ts-loader',
exclude: [/node_modules/],
}
],
},
plugins: [
new HtmlWebpackPlugin({ template: './src/index.html' }),
new ModuleFederationPlugin({
name: 'remote',
library: { type: 'var', name: 'remote' },
filename: 'remoteEntry.js',
shared: {
'react': { singleton: true, eager: true } ,
'react-dom': { singleton: true, eager: true }
}
}),
],
};
3. Add run script to your package.json to run the server.
{
"name": "remote_app",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "webpack serve"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"react": "^17.0.2",
"react-dom": "^17.0.2"
},
"devDependencies": {
"@types/react": "^17.0.30",
"@types/react-dom": "^17.0.11",
"@types/react-router-config": "^5.0.3",
"@types/react-router-dom": "^5.3.2"
"html-webpack-plugin": "^5.4.0",
"ts-loader": "^9.2.6",
"typescript": "^4.5.2",
"webpack": "^5.58.1",
"webpack-cli": "^4.9.1",
"webpack-dev-server": "^3.11.2"
}
}
4. Setup entry file. (src/index.ts). According to webpack documentation this will create an asynchronous
boundary to improve performance.
import('./bootstrap');
export {};
5. Setup boostrap.ts(src/bootstrap.ts)
import React from 'react';
import ReactDOM from 'react-dom';
6. Run the application(http://localhost:3000)
Container App Setup
- Install webpack 5 in your application
npm i webpack webpack-cli webpack-dev-server
2. Create a webpack.config.js file
const path = require("path");
const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const ModuleFederationPlugin = require("webpack").container.ModuleFederationPlugin;
module.exports = {
// Where files should be sent once they are bundled
entry: './src/index.ts',
output: {
publicPath: '/host',
path: path.join(__dirname, '/dist'),
filename: 'index.[hash].js',
},
// webpack 5 comes with devServer which loads in development mode
devServer: {
host: '0.0.0.0',
port: 3001,
open: true,
},
resolve: {
extensions: ['.js', '.jsx', '.ts', '.tsx']
},
// Rules of how webpack will take our files, complie & bundle them for the browser
module: {
rules: [
{
test: /\.(ts|tsx)?$/,
loader: 'ts-loader',
exclude: [/node_modules/],
}
],
},
plugins: [
new HtmlWebpackPlugin({ template: './src/index.html' }),
new ModuleFederationPlugin({
name: "host",
remoteType: 'var',
remotes: {
remote: "remote" //remote module
},
shared: {
'react': { singleton: true, eager: true } ,
'react-dom': { singleton: true, eager: true }
}
}),
],
};
3. Add run script to your package.json to run the server.
{
"name": "host_app",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "webpack serve"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"react": "^17.0.2",
"react-dom": "^17.0.2"
},
"devDependencies": {
"@types/react": "^17.0.30",
"@types/react-dom": "^17.0.11",
"@types/react-router-config": "^5.0.3",
"@types/react-router-dom": "^5.3.2"
"html-webpack-plugin": "^5.4.0",
"ts-loader": "^9.2.6",
"typescript": "^4.5.2",
"webpack": "^5.58.1",
"webpack-cli": "^4.9.1",
"webpack-dev-server": "^3.11.2"
}
}
4. Update index.html file to include the remote module
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Host App</title>
<script src="http://localhost:3000/remote/remoteEntry.js"></script
</head> <body>
<div id="app"></div>
</body>
</html>
5. Setup entry file. (src/index.ts).
import('./bootstrap');
export {};
6. Setup boostrap.ts(src/bootstrap.ts)
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App.tsx'
ReactDOM.render(<App/>, document.getElementById('app'));
7. Run the application (http://localhost:3001)
Consuming common components
Once basic setup is done, we can extend the remote module to create remote components.
- Create a component in remote application (src/RemoteApp.tsx).
import React from 'react';
//Imports
const RemoteApp = ({children }) => {
return <div>Just a remote module</div>;
};
export default RemoteApp;
2. Expose the component through module federation plugin (Remote → webpack.config.js)
...
plugins: [
new HtmlWebpackPlugin({ template: './src/index.html' }),
new ModuleFederationPlugin({
name: 'remote',
library: { type: 'var', name: 'remote' },
filename: 'remoteEntry.js',
exposes: {
"./RemoteApp": "./src/RemoteApp.tsx",
}
shared: {
'react': { singleton: true, eager: true } ,
'react-dom': { singleton: true, eager: true }
}
}),
],
...
3. Restart the remote app (http://localhost:3000)
4. Consume the component in container app. (host → src/App.tsx)
import React from 'react';//Imports
const RemoteApp = React.lazy(() => import('remote/RemoteApp'));
const App = () => {
return (
<React.Suspense fallback="loading">
<RemoteApp>
</RemoteApp>
</React.Suspense>
)
};export default App;
5. You should see the remote module on host screen (http://localhost:3001)
Consuming common services
- Create a remote service inside remote module (Remote → src/remoteService.ts)
class RemoteService {
init = () => {
console.log("remote service called");
}
}
const remoteService = new RemoteService();
export default remoteService;
2. Expose the service through module federation plugin
...
plugins: [
new HtmlWebpackPlugin({ template: './src/index.html' }),
new ModuleFederationPlugin({
name: 'remote',
library: { type: 'var', name: 'remote' },
filename: 'remoteEntry.js',
exposes: {
"./RemoteApp": "./src/RemoteApp.tsx",
"./remoteService": "./src/remoteService.ts"
}
shared: {
'react': { singleton: true, eager: true } ,
'react-dom': { singleton: true, eager: true }
}
}),
],
...
3. Restart the remote application (http://localhost:3000)
4. Consuming service in host application
import React, { useEffect } from 'react';
//Imports
const RemoteApp = React.lazy(() => import('remote/RemoteApp'));
import remoteService from 'remote/remoteService';
const App = () => {
useEffect(() => {
remoteService.init();
}, []);
return (
<React.Suspense fallback="loading">
<RemoteApp>
</RemoteApp>
</React.Suspense>
)
};
export default App;
5. You should see the console.log from remoteService in your browser where host application is running(http://localhost:3001)
Deploying remote module to cloud and consuming in host application
Above example runs the remote in localhost, but in real application you might be hosting the remote module to a cloud offering like AWS.
- Build remote application with webpack and deploy to cloud provider. e.g. http://cloud-provider.com/remote
2. Now remote entry file is available on cloud, from step1 it should be on e.g. http://cloud-provider.com/remote/remoteEntry.js
3. Update host(src/index.html) to include the remoteEntry file.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Host App</title>
<script src="http://cloud-provider.com/remote/remoteEntry.js"></script
</head>
<body>
<div id="app"></div>
</body>
</html>
Common Issues
Working with typescript
- You need to add a type(.d.ts) in host application to work with remote types (Host → src/global.d.ts)
/// <reference types="react" />
declare module "remote/RemoteApp" {
const RemoteApp: React.ComponentType;
export default RemoteApp;
}
declare module "remote/remoteService" {
}
Loading modules async
In above example we loaded remoteService synchronously in host application. Another way is to load the service async
const remoteService = await import('remote/remoteService');