Building Micro Frontends with React: Breaking a Monolithic App into Independent Pieces



What Are Micro Frontends?
Micro frontends extend the concept of microservices to the frontend world. Instead of building one large, interdependent codebase, you split your application into smaller, self-contained modules that can be developed, deployed, and maintained independently. Each micro frontend represents a distinct feature or section of your application and can even be built using different technologies if needed.
Benefits of Micro Frontends
- Independent Development and Deployment:
Teams can work autonomously on different parts of the UI. This reduces dependencies and accelerates development cycles. - Scalability:
Smaller codebases are easier to scale and maintain. Each micro frontend can be scaled individually based on usage patterns. - Resilience:
Isolating parts of your application helps contain failures. A problem in one micro frontend doesn’t necessarily bring down the entire app. - Technology Agnosticism:
While we focus on React in this tutorial, micro frontends allow you to integrate different frameworks or libraries into one cohesive application.
Micro Frontend Architecture
A typical micro frontend architecture consists of a container (or shell) and multiple micro frontend applications. The container acts as the orchestrator—it loads, composes, and manages the lifecycle of the micro frontends. One modern approach to achieve this is Webpack 5’s Module Federation plugin, which enables sharing code and dependencies between independently deployed builds.
Key Architectural Components:
- Container App:
The main application that loads and renders micro frontends dynamically. - Remote Apps (Micro Frontends):
Independent modules that expose parts of their code to be consumed by the container. - Shared Dependencies:
Libraries (like React and ReactDOM) are shared to avoid duplication and ensure consistency.
Tutorial: Breaking a Monolithic React App into Micro Frontends
We’ll build a simple setup with two projects:
- Container App: Runs on
http://localhost:3000
- Micro App: Runs on
http://localhost:3001
and exposes a simple React component
1. Setting Up the Container App
a. Webpack Configuration
Create a webpack.config.js
for your container app:
// container/webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { ModuleFederationPlugin } = require('webpack').container;
const deps = require('./package.json').dependencies;
module.exports = {
mode: 'development',
devServer: {
port: 3000,
},
output: {
publicPath: 'http://localhost:3000/',
},
module: {
rules: [
{
test: /\.jsx?$/,
loader: 'babel-loader',
exclude: /node_modules/,
},
],
},
plugins: [
new ModuleFederationPlugin({
name: 'container',
remotes: {
microApp: 'microApp@http://localhost:3001/remoteEntry.js',
},
shared: {
react: { singleton: true, requiredVersion: deps.react },
'react-dom': { singleton: true, requiredVersion: deps['react-dom'] },
},
}),
new HtmlWebpackPlugin({
template: './public/index.html',
}),
],
};
b. Container’s React Application
Create a simple React application that lazy-loads the micro frontend:
// container/src/App.js
import React, { Suspense } from 'react';
const MicroApp = React.lazy(() => import('microApp/MicroApp'));
function App() {
return (
<div>
<h1>Container App</h1>
<Suspense fallback={<div>Loading Micro Frontend...</div>}>
<MicroApp />
</Suspense>
</div>
);
}
export default App;
And your entry point (e.g., index.js
) might look like:
// container/src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(<App />, document.getElementById('root'));
2. Setting Up the Micro App
a. Webpack Configuration
Create a webpack.config.js
for the micro frontend:
// micro-app/webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { ModuleFederationPlugin } = require('webpack').container;
const deps = require('./package.json').dependencies;
module.exports = {
mode: 'development',
devServer: {
port: 3001,
},
output: {
publicPath: 'http://localhost:3001/',
},
module: {
rules: [
{
test: /\.jsx?$/,
loader: 'babel-loader',
exclude: /node_modules/,
},
],
},
plugins: [
new ModuleFederationPlugin({
name: 'microApp',
filename: 'remoteEntry.js',
exposes: {
'./MicroApp': './src/MicroApp',
},
shared: {
react: { singleton: true, requiredVersion: deps.react },
'react-dom': { singleton: true, requiredVersion: deps['react-dom'] },
},
}),
new HtmlWebpackPlugin({
template: './public/index.html',
}),
],
};
b. Micro App’s React Component
Build a simple component that the container will load:
// micro-app/src/MicroApp.js
import React from 'react';
const MicroApp = () => {
return (
<div>
<h2>Micro Frontend</h2>
<p>This micro frontend is independently deployed and managed.</p>
</div>
);
};
export default MicroApp;
And the entry point for the micro app:
// micro-app/src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import MicroApp from './MicroApp';
ReactDOM.render(<MicroApp />, document.getElementById('root'));
3. Running the Projects
- Start the Micro App:
In themicro-app
directory, run your development server (e.g., viawebpack serve
or a similar command) so it listens on port 3001. - Start the Container App:
Similarly, in thecontainer
directory, start its development server so it listens on port 3000. - Access the Application:
Open your browser to http://localhost:3000. The container app will load and display the micro frontend from the micro app.
Conclusion
By splitting a monolithic React application into micro frontends, you unlock the power of independent development, faster deployments, and improved scalability. In this tutorial, we demonstrated how to set up a container and a micro frontend using Webpack 5’s Module Federation. This modular approach not only simplifies maintenance but also paves the way for seamless team collaboration and technology diversity.
Feel free to experiment further by adding more micro frontends, integrating additional shared libraries, or even exploring different frameworks within the same application. Happy coding!