Micro-frontends are an architectural pattern that extends the microservices concept to the frontend, enabling independent teams to develop, deploy, and scale individual parts of a frontend application.
Micro-frontends are a technique for splitting a monolithic frontend application into smaller, independent applications that can be developed by different teams and deployed independently.
1// webpack.config.js - Shell Application
2const ModuleFederationPlugin = require('@module-federation/webpack');
3
4module.exports = {
5 mode: 'development',
6 devServer: {
7 port: 3000,
8 },
9 plugins: [
10 new ModuleFederationPlugin({
11 name: 'shell',
12 remotes: {
13 products: 'products@http://localhost:3001/remoteEntry.js',
14 cart: 'cart@http://localhost:3002/remoteEntry.js',
15 user: 'user@http://localhost:3003/remoteEntry.js',
16 },
17 shared: {
18 react: { singleton: true, eager: true },
19 'react-dom': { singleton: true, eager: true },
20 '@shared/ui-kit': { singleton: true },
21 '@shared/utils': { singleton: true },
22 },
23 }),
24 ],
25};1// src/App.jsx - Shell Application
2import React, { Suspense, lazy } from 'react';
3import { BrowserRouter, Routes, Route } from 'react-router-dom';
4import ErrorBoundary from './components/ErrorBoundary';
5import Layout from './components/Layout';
6
7// Lazy loading Micro-frontends
8const ProductsApp = lazy(() => import('products/ProductsApp'));
9const CartApp = lazy(() => import('cart/CartApp'));
10const UserApp = lazy(() => import('user/UserApp'));
11
12// Local components
13const HomePage = lazy(() => import('./pages/HomePage'));
14
15function App() {
16 return (
17 <ErrorBoundary>
18 <BrowserRouter>
19 <Layout>
20 <Suspense fallback={<div>Loading...</div>}>
21 <Routes>
22 <Route path="/" element={<HomePage />} />
23
24 {/* Products Micro-frontend */}
25 <Route
26 path="/products/*"
27 element={
28 <ErrorBoundary fallback={<div>Products unavailable</div>}>
29 <ProductsApp />
30 </ErrorBoundary>
31 }
32 />
33
34 {/* Cart Micro-frontend */}
35 <Route
36 path="/cart/*"
37 element={
38 <ErrorBoundary fallback={<div>Cart unavailable</div>}>
39 <CartApp />
40 </ErrorBoundary>
41 }
42 />
43
44 {/* User Micro-frontend */}
45 <Route
46 path="/user/*"
47 element={
48 <ErrorBoundary fallback={<div>User profile unavailable</div>}>
49 <UserApp />
50 </ErrorBoundary>
51 }
52 />
53 </Routes>
54 </Suspense>
55 </Layout>
56 </BrowserRouter>
57 </ErrorBoundary>
58 );
59}
60
61export default App;1// shared/eventBus.js
2class EventBus {
3 constructor() {
4 this.events = {};
5 }
6
7 subscribe(eventName, callback) {
8 if (!this.events[eventName]) {
9 this.events[eventName] = [];
10 }
11 this.events[eventName].push(callback);
12
13 // Return unsubscribe function
14 return () => {
15 this.events[eventName] = this.events[eventName].filter(
16 cb => cb !== callback
17 );
18 };
19 }
20
21 emit(eventName, data) {
22 if (this.events[eventName]) {
23 this.events[eventName].forEach(callback => callback(data));
24 }
25
26 // Also emit as CustomEvent for cross-MFE communication
27 window.dispatchEvent(new CustomEvent(`mfe:${eventName}`, {
28 detail: data
29 }));
30 }
31}
32
33export const eventBus = new EventBus();1// shared/stateManager.js
2import { createStore } from 'zustand/vanilla';
3
4// Global store accessible across MFEs
5const globalStore = createStore((set, get) => ({
6 // User state
7 user: null,
8 isAuthenticated: false,
9
10 // Cart state
11 cartItems: [],
12 cartTotal: 0,
13
14 // Actions
15 setUser: (user) => set({ user, isAuthenticated: !!user }),
16
17 addToCart: (product) => set(state => {
18 const existingItem = state.cartItems.find(item => item.id === product.id);
19 let newItems;
20
21 if (existingItem) {
22 newItems = state.cartItems.map(item =>
23 item.id === product.id
24 ? { ...item, quantity: item.quantity + 1 }
25 : item
26 );
27 } else {
28 newItems = [...state.cartItems, { ...product, quantity: 1 }];
29 }
30
31 const newTotal = newItems.reduce((sum, item) =>
32 sum + (item.price * item.quantity), 0
33 );
34
35 return { cartItems: newItems, cartTotal: newTotal };
36 }),
37
38 removeFromCart: (productId) => set(state => {
39 const newItems = state.cartItems.filter(item => item.id !== productId);
40 const newTotal = newItems.reduce((sum, item) =>
41 sum + (item.price * item.quantity), 0
42 );
43
44 return { cartItems: newItems, cartTotal: newTotal };
45 }),
46}));1// __tests__/integration/mfe-integration.test.jsx
2import { render, screen, waitFor } from '@testing-library/react';
3import userEvent from '@testing-library/user-event';
4import { BrowserRouter } from 'react-router-dom';
5import App from '../App';
6
7// Mock MFEs
8jest.mock('products/ProductsApp', () => {
9 return function MockProductsApp() {
10 return <div data-testid="products-mfe">Products MFE Loaded</div>;
11 };
12});
13
14describe('MFE Integration', () => {
15 test('loads products MFE when navigating to /products', async () => {
16 const user = userEvent.setup();
17
18 render(
19 <BrowserRouter>
20 <App />
21 </BrowserRouter>
22 );
23
24 // Navigate to products
25 await user.click(screen.getByRole('link', { name: /products/i }));
26
27 // Verify MFE is loaded
28 await waitFor(() => {
29 expect(screen.getByTestId('products-mfe')).toBeInTheDocument();
30 });
31 });
32});Micro-frontends offer powerful capabilities for scaling React applications in large organizations, but they require a well-thought-out architecture and good complexity management.