Creating Micro Frontends with React: A Comprehensive Guide
In recent years, the concept of micro frontends has emerged as a powerful architectural style for building scalable, maintainable web applications. By enabling teams to work independently on different parts of an application, micro frontends facilitate parallel development and improve the overall agility of software delivery. This article explores how to create micro frontends using React, a popular JavaScript library for building user interfaces.
What are Micro Frontends?
Micro frontends break a monolithic front-end application into smaller, manageable, and independently deployable pieces. Each piece represents a specific business domain or feature and can be developed, tested, and deployed by different teams. This allows for:
- Independent Development: Teams can work on their own features without waiting for others.
- Technology Agnostic: Different parts of the application can be built with different technologies.
- Scalability: The overall system can grow more easily as the application matures.
By leveraging micro frontends, organizations can adapt to changing requirements and scale their web applications more effectively.
Key Concepts in Micro Frontend Architecture
Before diving into the implementation, let’s review some key concepts associated with micro frontends:
- Single SPA: A framework for managing multiple micro frontends on one page.
- Module Federation: A Webpack 5 feature that allows dynamic code sharing between applications.
- Routing and Navigation: Effective strategies to handle routing across different micro frontends.
- Communication: Mechanisms to enable communication between micro frontends.
Setting Up Your Micro Frontend Environment
We’ll use React with Single SPA and Webpack Module Federation for our micro frontend setup. Begin by creating a new directory for our project:
mkdir react-micro-frontends
cd react-micro-frontends
Next, initialize a new npm project:
npm init -y
Install necessary dependencies:
npm install react react-dom single-spa
npm install --save-dev webpack webpack-cli webpack-dev-server html-webpack-plugin
Creating Your First Micro Frontend
Step 1: Creating a Micro Frontend Application
In our project, we’ll create two micro frontend applications, “app1” and “app2,” which can operate independently.
mkdir app1 app2
Each application will have a standard React structure:
app1/
├── src/
│ ├── index.js
│ └── App.js
├── public/
│ └── index.html
└── package.json
In the src/index.js of app1, add:
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(, document.getElementById('root'));
In the src/App.js, create a simple component:
import React from 'react';
const App = () => {
return (
<div>
<h1>Welcome to App 1</h1>
<p>This is the first micro frontend!</p>
</div>
);
};
export default App;
The public/index.html will need to load the script generated by Webpack:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>App 1</title>
</head>
<body>
<div id="root"></div>
</body>
Step 2: Configuring Webpack
We need to configure Webpack for each of our apps. Create a webpack.config.js file in the root of app1:
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { ModuleFederationPlugin } = require('webpack/lib/container');
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
filename: 'bundle.js',
publicPath: 'http://localhost:3001/',
},
devServer: {
port: 3001,
},
plugins: [
new ModuleFederationPlugin({
name: 'app1',
filename: 'remoteEntry.js',
exposes: {
'./App': './src/App',
},
shared: {
react: {
singleton: true,
eager: true,
requiredVersion: '^17.0.0'
},
'react-dom': {
singleton: true,
eager: true,
requiredVersion: '^17.0.0'
},
},
}),
new HtmlWebpackPlugin({
template: './public/index.html'
}),
],
module: {
rules: [
{
test: /.jsx?$/,
use: 'babel-loader',
exclude: /node_modules/,
},
],
},
};
Repeat the same for app2 while changing ports and filenames accordingly. In app2:
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { ModuleFederationPlugin } = require('webpack/lib/container');
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
filename: 'bundle.js',
publicPath: 'http://localhost:3002/',
},
devServer: {
port: 3002,
},
plugins: [
new ModuleFederationPlugin({
name: 'app2',
filename: 'remoteEntry.js',
exposes: {
'./App': './src/App',
},
shared: {
react: {
singleton: true,
eager: true,
requiredVersion: '^17.0.0'
},
'react-dom': {
singleton: true,
eager: true,
requiredVersion: '^17.0.0'
},
},
}),
new HtmlWebpackPlugin({
template: './public/index.html'
}),
],
module: {
rules: [
{
test: /.jsx?$/,
use: 'babel-loader',
exclude: /node_modules/,
},
],
},
};
Step 3: Setting Up the Container Application
Next, we need to create a *container* application that will load our micro frontends. Create a new directory for the container:
mkdir container
cd container
npm init -y
npm install react react-dom single-spa
npm install --save-dev webpack webpack-cli webpack-dev-server html-webpack-plugin
mkdir src public
Creating the Container Application
Add the following files similar to previous applications:
container/
├── src/
│ └── index.js
├── public/
│ └── index.html
└── webpack.config.js
In src/index.js, set up Single SPA:
import { registerApplication, start } from 'single-spa';
registerApplication(
'app1',
() => import('app1/App'),
() => true
);
registerApplication(
'app2',
() => import('app2/App'),
() => location.pathname === '/app2'
);
start();
In the public/index.html, define a root element:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Container App</title>
</head>
<body>
<div id="root"></div>
</body>
Configuring Webpack for the Container
Create a webpack.config.js file in the root of the container:
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { ModuleFederationPlugin } = require('webpack/lib/container');
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
filename: 'bundle.js',
publicPath: 'http://localhost:3000/',
},
devServer: {
port: 3000,
},
plugins: [
new ModuleFederationPlugin({
name: 'container',
remotes: {
app1: 'app1@http://localhost:3001/remoteEntry.js',
app2: 'app2@http://localhost:3002/remoteEntry.js',
},
shared: {
react: {
singleton: true,
eager: true,
requiredVersion: '^17.0.0'
},
'react-dom': {
singleton: true,
eager: true,
requiredVersion: '^17.0.0'
},
},
}),
new HtmlWebpackPlugin({
template: './public/index.html'
}),
],
module: {
rules: [
{
test: /.jsx?$/,
use: 'babel-loader',
exclude: /node_modules/,
},
],
},
};
Step 4: Running the Micro Frontends
Navigate to each directory (app1, app2, and container) and run:
npx webpack serve
This will start the server for each application on their respective ports (3001 for app1, 3002 for app2, and 3000 for container). Open the container app in your browser at http://localhost:3000 to see your micro frontends in action!
Handling Routing Across Micro Frontends
One of the challenges of using micro frontends is managing routing. The container can handle paths and serve appropriate micro frontends based on the URL. You can extend the registerApplication function in the container to include more complex logic for handling different routes.
Example of Advanced Routing Logic
registerApplication(
'app1',
() => import('app1/App'),
location => location.pathname === '' || location.pathname === '/app1'
);
registerApplication(
'app2',
() => import('app2/App'),
location => location.pathname === '/app2'
);
The example above checks the URL path and loads the relevant micro frontend accordingly.
Communication Between Micro Frontends
While micro frontends operate independently, there are scenarios where they need to communicate. One common approach is to use a central event bus, which can be implemented using JavaScript’s built-in event mechanism.
Example Event Bus Implementation
const bus = {
events: {},
subscribe(event, cb) {
this.events[event] = this.events[event] || [];
this.events[event].push(cb);
},
publish(event, data) {
if (this.events[event]) {
this.events[event].forEach(cb => cb(data));
}
}
};
// Example usage;
// One micro frontend can listen for an event
bus.subscribe('MY_EVENT', data => console.log(data));
// Another micro frontend can publish the event
bus.publish('MY_EVENT', { msg: 'Hello from app1!' });
Optimizing Micro Frontends
As with any architecture, performance is key. Here are some best practices for optimizing micro frontends:
- Code Splitting: Use dynamic imports in individual micro frontends to reduce initial load times.
- Shared Libraries: Minimize the size of shared libraries to avoid bloating the application.
- Lazy Loading: Implement lazy loading for routes that are not required immediately.
- Network Requests: Ensure that API calls are called only when necessary.
Conclusion
Micro frontends architecture provides a flexible and scalable approach to developing complex web applications. By using React and tools like Single SPA and Webpack Module Federation, developers can easily implement micro frontends that enhance inter-team collaboration and improve code maintainability. We hope this guide helps you get started with creating your own micro frontends in React!
Happy coding!
