Micro frontend architecture has emerged as a powerful approach for building scalable, maintainable frontend applications. By extending microservice principles to frontend development, development teams can work independently while creating a cohesive user experience. Let’s explore this architecture with React as our framework of choice.
What Are Micro Frontends?
Micro frontends break down frontend monoliths into smaller, independently deployable applications that together form a cohesive user experience. This architectural style allows:
- Teams to work autonomously on separate features
- Independent deployment cycles
- Technology flexibility within agreed constraints
- Better fault isolation
- Easier maintenance and scaling
Core Implementation Approaches
There are several ways to implement micro frontends with React:
1. Build-Time Integration
With build-time integration, micro frontends are published as packages and imported into a container application.
2. Run-Time Integration via iframes
Using iframes to isolate micro frontends provides strong encapsulation but comes with limitations around communication and styling.
3. Run-Time Integration via JavaScript
This approach dynamically loads micro frontend JavaScript bundles into the browser at runtime.
4. Web Components
Wrapping React micro frontends in custom elements (Web Components) creates framework-agnostic components that can be used anywhere.
Practical Implementation Example
Let’s explore a run-time JavaScript integration approach using Module Federation, a webpack 5 feature that enables sharing code between applications.
React Micro Frontend Architecture Example
Code
// Project Structure:
// root/
// ├── container/ # Container application
// ├── product-list/ # Product listing micro frontend
// └── product-detail/ # Product detail micro frontend
// --- CONTAINER APP ---
// container/webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
module.exports = {
// ... other webpack config
plugins: [
new ModuleFederationPlugin({
name: 'container',
remotes: {
productList: 'productList@http://localhost:3001/remoteEntry.js',
productDetail: 'productDetail@http://localhost:3002/remoteEntry.js',
},
shared: ['react', 'react-dom', 'react-router-dom'],
}),
new HtmlWebpackPlugin({
template: './public/index.html',
}),
],
};
// container/src/App.js
import React, { lazy, Suspense } from 'react';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import Header from './components/Header';
// Lazy load the micro frontends
const ProductList = lazy(() => import('productList/ProductListApp'));
const ProductDetail = lazy(() => import('productDetail/ProductDetailApp'));
const App = () => {
return (
<BrowserRouter>
<Header />
<Suspense fallback={<div>Loading...</div>}>
<Switch>
<Route exact path="/" component={ProductList} />
<Route path="/product/:id" component={ProductDetail} />
</Switch>
</Suspense>
</BrowserRouter>
);
};
export default App;
// --- PRODUCT LIST MICRO FRONTEND ---
// product-list/webpack.config.js
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
module.exports = {
// ... other webpack config
plugins: [
new ModuleFederationPlugin({
name: 'productList',
filename: 'remoteEntry.js',
exposes: {
'./ProductListApp': './src/App',
},
shared: ['react', 'react-dom', 'react-router-dom'],
}),
],
};
// product-list/src/App.js
import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
const ProductListApp = () => {
const [products, setProducts] = useState([]);
useEffect(() => {
// Fetch product data
fetch('https://api.example.com/products')
.then(res => res.json())
.then(data => setProducts(data));
}, []);
return (
<div className="product-list">
<h1>Products</h1>
<ul>
{products.map(product => (
<li key={product.id}>
<Link to={`/product/${product.id}`}>{product.name}</Link>
</li>
))}
</ul>
</div>
);
};
export default ProductListApp;
// --- PRODUCT DETAIL MICRO FRONTEND ---
// product-detail/webpack.config.js
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
module.exports = {
// ... other webpack config
plugins: [
new ModuleFederationPlugin({
name: 'productDetail',
filename: 'remoteEntry.js',
exposes: {
'./ProductDetailApp': './src/App',
},
shared: ['react', 'react-dom', 'react-router-dom'],
}),
],
};
// product-detail/src/App.js
import React, { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom';
const ProductDetailApp = () => {
const [product, setProduct] = useState(null);
const { id } = useParams();
useEffect(() => {
fetch(`https://api.example.com/products/${id}`)
.then(res => res.json())
.then(data => setProduct(data));
}, [id]);
if (!product) return <div>Loading product...</div>;
return (
<div className="product-detail">
<h1>{product.name}</h1>
<p>{product.description}</p>
<p>Price: ${product.price}</p>
<button>Add to Cart</button>
</div>
);
};
export default ProductDetailApp;
// --- SHARED COMMUNICATION USING EVENT BUS ---
// To enable communication between micro frontends, we can implement a simple event bus
// shared/event-bus.js (can be published as an npm package)
class EventBus {
constructor() {
this.events = {};
}
on(event, callback) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(callback);
}
emit(event, data) {
if (this.events[event]) {
this.events[event].forEach(callback => callback(data));
}
}
off(event, callback) {
if (this.events[event]) {
this.events[event] = this.events[event].filter(cb => cb !== callback);
}
}
}
export default new EventBus();
// Usage in product-detail:
import eventBus from 'shared/event-bus';
const addToCart = (product) => {
eventBus.emit('ADD_TO_CART', product);
};
// Usage in header component (in container app):
import eventBus from 'shared/event-bus';
useEffect(() => {
const handleAddToCart = (product) => {
// Update cart state
setCartItems(prev => [...prev, product]);
};
eventBus.on('ADD_TO_CART', handleAddToCart);
return () => {
eventBus.off('ADD_TO_CART', handleAddToCart);
};
}, []);
Key Challenges and Solutions
1. Styling Consistency
Maintaining visual consistency across micro frontends is crucial. Consider these approaches:
- Shared Design System: Create a common UI library that all teams use
- CSS-in-JS Solutions: Tools like styled-components with a shared theme
- CSS Variables: Define global variables for colors, spacing, etc.
2. Authentication and State Management
Handling shared state across micro frontends requires careful planning:
- JWT Tokens: Store authentication tokens in localStorage/cookies
- Global State Library: Redux, MobX, or Zustand with shared stores
- Custom Event Bus: For cross-micro frontend communication
- Backend for Frontend (BFF): API gateway pattern to handle aggregation
3. Routing
Coordinating navigation between micro frontends:
- Shell Application Routing: Container app handles primary routing
- Hash-Based Routing: Each micro frontend manages its routes with hash fragments
- History API Integration: Shared history object across applications
Advanced Implementation: Single-SPA
For more complex scenarios, consider Single-SPA, a framework for JavaScript micro frontends:
root-config.js
import { registerApplication, start } from 'single-spa';
registerApplication({
name: 'product-list',
app: () => import('@company/product-list'),
activeWhen: ['/products']
});
registerApplication({
name: 'product-detail',
app: () => import('@company/product-detail'),
activeWhen: ['/product/:id']
});
start();
Best Practices
- Define Clear Boundaries: Each micro frontend should align with a business domain
- Agree on Technical Standards: Establish shared conventions for routing, styling, etc.
- Optimize Bundle Size: Use shared dependencies to prevent duplicate libraries
- Implement Monitoring: Track performance across all micro frontends
- Progressive Migration: Convert monoliths to micro frontends incrementally
Real-World Example: E-commerce Site
An e-commerce application might be broken down into these micro frontends:
- Header/Navigation: Common UI elements and search
- Product Discovery: Search results and browsing
- Product Detail: Individual product information
- Shopping Cart: Cart management
- Checkout Flow: Payment processing
- User Account: Profile management
Each team can work independently while contributing to the overall application.
Conclusion
React micro frontend architecture offers significant benefits for large teams and complex applications. While there are challenges to overcome, the architecture enables greater team autonomy, more frequent deployments, and better maintainability.
By choosing the right integration strategy, establishing clear communication patterns, and following best practices, you can successfully implement a micro frontend architecture that scales with your organization’s needs.
At 7Shades Digital, we specialised in creating strategies that help businesses excel in the digital world. If you’re ready to take your website to the next level, contact us today!