Micro Frontends · Additional notes
1 min readRapid overview
- Additional notes
- Communication Between Micro Frontends
- 1. Custom Events
- 2. Shared Event Bus
- 3. Shared State (Redux, Zustand)
- 4. Props/Callbacks (Parent-Child)
- Routing in Micro Frontends
- Centralized Routing (Shell Controls)
- Decentralized Routing (MFEs Control Own Routes)
- Styling Strategies
- 1. CSS Modules / Scoped Styles
- 2. CSS-in-JS with Unique Class Names
- 3. Shadow DOM (Web Components)
- 4. Design System with CSS Variables
- Best Practices
- 1. Define Clear Boundaries
- 2. Shared Dependencies
- 3. Versioning Strategy
- 4. Error Boundaries
- 5. Performance Monitoring
- Testing Strategies
- Unit Testing
- Integration Testing
- E2E Testing
- Tools and Frameworks
- Single-SPA
- Module Federation
- Nx
- Bit
Additional notes
Communication Between Micro Frontends
1. Custom Events
// Micro Frontend A - Dispatch event
const event = new CustomEvent('cart:updated', {
detail: { itemCount: 5, total: 99.99 }
});
window.dispatchEvent(event);
// Micro Frontend B - Listen for event
window.addEventListener('cart:updated', (event) => {
console.log('Cart updated:', event.detail);
updateCartBadge(event.detail.itemCount);
});
2. Shared Event Bus
// eventBus.js (shared library)
class EventBus {
constructor() {
this.events = {};
}
on(event, callback) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(callback);
return () => this.off(event, callback);
}
off(event, callback) {
if (this.events[event]) {
this.events[event] = this.events[event].filter(cb => cb !== callback);
}
}
emit(event, data) {
if (this.events[event]) {
this.events[event].forEach(callback => callback(data));
}
}
}
export const eventBus = new EventBus();
// Usage in MFE A
import { eventBus } from '@company/shared-event-bus';
eventBus.emit('user:login', { userId: 123, name: 'John' });
// Usage in MFE B
import { eventBus } from '@company/shared-event-bus';
eventBus.on('user:login', (user) => {
console.log('User logged in:', user);
});
3. Shared State (Redux, Zustand)
// Shared store
import create from 'zustand';
export const useGlobalStore = create((set) => ({
user: null,
cart: [],
setUser: (user) => set({ user }),
addToCart: (item) => set((state) => ({ cart: [...state.cart, item] }))
}));
// MFE A
import { useGlobalStore } from '@company/shared-store';
function Header() {
const user = useGlobalStore(state => state.user);
return <div>Welcome, {user?.name}</div>;
}
// MFE B
import { useGlobalStore } from '@company/shared-store';
function ProductCard({ product }) {
const addToCart = useGlobalStore(state => state.addToCart);
return (
<button onClick={() => addToCart(product)}>
Add to Cart
</button>
);
}
4. Props/Callbacks (Parent-Child)
// Shell App
import ProductList from 'products/ProductList';
function App() {
const [selectedProduct, setSelectedProduct] = useState(null);
return (
<div>
<ProductList onProductSelect={setSelectedProduct} />
{selectedProduct && <ProductDetails product={selectedProduct} />}
</div>
);
}
Routing in Micro Frontends
Centralized Routing (Shell Controls)
// Shell App with React Router
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { lazy, Suspense } from 'react';
const Header = lazy(() => import('header/Header'));
const Products = lazy(() => import('products/ProductList'));
const ProductDetail = lazy(() => import('products/ProductDetail'));
const Checkout = lazy(() => import('checkout/CheckoutFlow'));
function App() {
return (
<BrowserRouter>
<Suspense fallback={<div>Loading...</div>}>
<Header />
<Routes>
<Route path="/" element={<Products />} />
<Route path="/products/:id" element={<ProductDetail />} />
<Route path="/checkout" element={<Checkout />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}
Decentralized Routing (MFEs Control Own Routes)
// Shell App
import { BrowserRouter, Routes, Route } from 'react-router-dom';
function App() {
return (
<BrowserRouter>
<Header />
<Routes>
<Route path="/products/*" element={<ProductsApp />} />
<Route path="/checkout/*" element={<CheckoutApp />} />
<Route path="/account/*" element={<AccountApp />} />
</Routes>
</BrowserRouter>
);
}
// Products MFE (internally manages /products/*)
import { Routes, Route } from 'react-router-dom';
function ProductsApp() {
return (
<Routes>
<Route index element={<ProductList />} />
<Route path=":id" element={<ProductDetail />} />
<Route path="search" element={<ProductSearch />} />
</Routes>
);
}
Styling Strategies
1. CSS Modules / Scoped Styles
// ProductCard.module.css
.card {
border: 1px solid #ccc;
padding: 1rem;
}
// ProductCard.jsx
import styles from './ProductCard.module.css';
function ProductCard({ product }) {
return <div className={styles.card}>{product.name}</div>;
}
2. CSS-in-JS with Unique Class Names
import styled from 'styled-components';
// Each MFE uses a unique prefix
const Card = styled.div.withConfig({
displayName: 'ProductCard' // Generates unique class
})`
border: 1px solid #ccc;
padding: 1rem;
`;
3. Shadow DOM (Web Components)
class ProductCard extends HTMLElement {
connectedCallback() {
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<style>
/* Styles are scoped to this component */
.card { border: 1px solid #ccc; }
</style>
<div class="card">Product</div>
`;
}
}
4. Design System with CSS Variables
/* Shared design tokens */
:root {
--color-primary: #007bff;
--color-secondary: #6c757d;
--spacing-unit: 8px;
--font-family: 'Inter', sans-serif;
}
/* Each MFE uses the tokens */
.product-card {
color: var(--color-primary);
padding: calc(var(--spacing-unit) * 2);
font-family: var(--font-family);
}
Best Practices
1. Define Clear Boundaries
- Each micro frontend should own a specific business domain
- Avoid splitting UI components arbitrarily
- Use vertical slicing (features) not horizontal (layers)
2. Shared Dependencies
// Share common libraries to reduce bundle size
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
'@company/design-system': { singleton: true }
}
3. Versioning Strategy
// Semantic versioning for micro frontends
{
"name": "@company/header-mfe",
"version": "2.1.4",
// Major: Breaking changes
// Minor: New features
// Patch: Bug fixes
}
4. Error Boundaries
class MicroFrontendErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error('MFE Error:', error, errorInfo);
// Log to error tracking service
}
render() {
if (this.state.hasError) {
return <div>This section is temporarily unavailable</div>;
}
return this.props.children;
}
}
// Wrap each MFE
<MicroFrontendErrorBoundary>
<ProductsMFE />
</MicroFrontendErrorBoundary>
5. Performance Monitoring
// Track MFE load times
const loadMFE = async (name, url) => {
const startTime = performance.now();
try {
await loadRemoteModule(name, url);
const loadTime = performance.now() - startTime;
// Send to analytics
analytics.track('mfe_loaded', {
name,
loadTime,
success: true
});
} catch (error) {
analytics.track('mfe_load_failed', {
name,
error: error.message
});
}
};
Testing Strategies
Unit Testing
Test each micro frontend independently.
// ProductCard.test.jsx
import { render, screen } from '@testing-library/react';
import ProductCard from './ProductCard';
test('displays product name', () => {
const product = { id: 1, name: 'Widget', price: 9.99 };
render(<ProductCard product={product} />);
expect(screen.getByText('Widget')).toBeInTheDocument();
});
Integration Testing
Test communication between micro frontends.
// Integration test
import { render, fireEvent } from '@testing-library/react';
import { eventBus } from '@company/shared-event-bus';
test('cart updates when product added', () => {
const cartListener = jest.fn();
eventBus.on('cart:updated', cartListener);
const { getByText } = render(<ProductCard product={product} />);
fireEvent.click(getByText('Add to Cart'));
expect(cartListener).toHaveBeenCalledWith({
itemCount: 1,
items: [product]
});
});
E2E Testing
Test the composed application.
// Cypress E2E test
describe('Product Purchase Flow', () => {
it('completes full purchase', () => {
cy.visit('/products');
cy.get('[data-testid="product-1"]').click();
cy.get('[data-testid="add-to-cart"]').click();
cy.get('[data-testid="cart-badge"]').should('contain', '1');
cy.get('[data-testid="checkout-button"]').click();
cy.url().should('include', '/checkout');
});
});
Tools and Frameworks
Single-SPA
Framework-agnostic micro frontend framework.
import { registerApplication, start } from 'single-spa';
registerApplication({
name: '@company/products',
app: () => import('@company/products'),
activeWhen: '/products'
});
start();
Module Federation
Webpack 5's built-in solution.
Nx
Monorepo tool with micro frontend support.
nx generate @nrwl/react:host shell
nx generate @nrwl/react:remote products
Bit
Component-driven micro frontends.
bit export @company/header
bit import @company/header