@unidev-hub/mfe-toolkit
v0.1.1
Published
Toolkit for micro-frontend architecture integration
Readme
@unidev-hub/mfe-toolkit
A comprehensive toolkit for micro-frontend (MFE) architecture integration in React applications.
📚 Table of Contents
- Features
- Installation
- Quick Start
- Core Concepts
- Components
- Hooks
- Communication
- Utils
- Examples
- Contributing
- License
✨ Features
- 🧩 MFE Integration Components: Easily integrate micro-frontends with consistent loading and error handling
- 🔄 Cross-MFE Communication: Event bus for seamless communication between MFEs
- 🌐 Shared State: Share state between MFEs with React-like APIs
- 🧭 Navigation Utilities: Handle navigation across MFE boundaries
- 📦 MFE Registry: Centralized registry for all MFEs in your application
- ⚠️ Error Handling: Specialized error boundaries for MFEs
- 🔌 Module Federation Support: First-class support for Webpack Module Federation
🔧 Installation
NPM
npm install @unidev-hub/mfe-toolkit react react-dom react-router-dom @mantine/coreYarn
yarn add @unidev-hub/mfe-toolkit react react-dom react-router-dom @mantine/corePNPM
pnpm add @unidev-hub/mfe-toolkit react react-dom react-router-dom @mantine/core🚀 Quick Start
1. Set up the MFE Communication Provider
First, set up the MFE Communication Provider in your shell application:
import React from 'react';
import { BrowserRouter } from 'react-router-dom';
import { MantineProvider } from '@mantine/core';
import { MfeCommunicationProvider } from '@unidev-hub/mfe-toolkit';
function App() {
return (
<MantineProvider>
<BrowserRouter>
<MfeCommunicationProvider
shellName="my-app-shell"
shellVersion="1.0.0"
initialRegistry={{
products: {
name: 'products',
scope: 'products',
module: 'ProductsApp'
},
vendors: {
name: 'vendors',
scope: 'vendors',
module: 'VendorsApp'
}
}}
>
<YourAppContent />
</MfeCommunicationProvider>
</BrowserRouter>
</MantineProvider>
);
}2. Load MFEs using MfeContainer
Use the MfeContainer component to load and integrate micro-frontends:
import React from 'react';
import { Routes, Route } from 'react-router-dom';
import { MfeContainer } from '@unidev-hub/mfe-toolkit';
function AppRoutes() {
return (
<Routes>
<Route path="/" element={<Dashboard />} />
<Route
path="/products/*"
element={
<MfeContainer
name="products"
module="ProductsApp"
onError={(err) => console.error("Products MFE failed to load:", err)}
/>
}
/>
<Route
path="/vendors/*"
element={
<MfeContainer
name="vendors"
module="VendorsApp"
/>
}
/>
</Routes>
);
}3. Communicate Between MFEs
Use the event bus to communicate between MFEs:
// In the Products MFE
import { useMfeEvents } from '@unidev-hub/mfe-toolkit';
function ProductDetail({ product }) {
const { emit } = useMfeEvents('products');
const addToCart = () => {
emit('cart:add-item', {
id: product.id,
name: product.name,
price: product.price
});
};
return (
<div>
<h2>{product.name}</h2>
<button onClick={addToCart}>Add to Cart</button>
</div>
);
}
// In the Cart MFE
import { useMfeEvents } from '@unidev-hub/mfe-toolkit';
function Cart() {
const [items, setItems] = useState([]);
const { on } = useMfeEvents('cart');
useEffect(() => {
const unsubscribe = on('cart:add-item', (product) => {
setItems(prev => [...prev, product]);
});
return unsubscribe;
}, [on]);
return (
<div>
<h2>Cart ({items.length} items)</h2>
{/* Render cart items */}
</div>
);
}4. Share State Between MFEs
Use the shared state to share data between MFEs:
// In the User MFE
import { useMfeState } from '@unidev-hub/mfe-toolkit';
function UserProfile() {
const [user, setUser] = useMfeState('user', 'currentUser', null);
useEffect(() => {
// Load user data
api.getUser().then(userData => {
setUser(userData);
});
}, []);
return (
<div>
{user ? (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
) : (
<div>Loading user...</div>
)}
</div>
);
}
// In any other MFE
import { useMfeStateValue } from '@unidev-hub/mfe-toolkit';
function UserGreeting() {
const user = useMfeStateValue('user', 'currentUser');
return (
<div>
{user ? `Hello, ${user.name}!` : 'Welcome, Guest!'}
</div>
);
}🧠 Core Concepts
Micro-Frontend Architecture
This toolkit is designed to support a micro-frontend architecture where:
- A Shell Application serves as the container for multiple micro-frontends
- Micro-Frontends (MFEs) are independent applications that integrate with the shell
- MFEs can communicate with each other and the shell through events and shared state
- MFEs can be loaded dynamically at runtime
Communication Patterns
The toolkit supports several communication patterns:
- Event-based: Using the event bus for loose coupling between MFEs
- Shared State: For more direct state sharing with React-like APIs
- Context-based: For global context shared across MFEs
Module Federation
The toolkit is designed to work with Webpack Module Federation, which allows:
- Dynamic loading of MFEs at runtime
- Shared dependencies between MFEs
- Independent deployment of MFEs
📦 Components
MfeContainer
A wrapper component that handles loading and rendering micro-frontends:
<MfeContainer
name="products" // Name of the MFE (must match registry entry)
module="ProductsApp" // Module to load from the MFE
mfeProps={{ // Props to pass to the MFE
theme: 'light',
showFilters: true
}}
showLoading={true} // Whether to show loading indicator
onError={(err) => { // Handler for load errors
console.error(err);
}}
onLoad={() => { // Handler for successful loads
console.log('MFE loaded successfully');
}}
/>MfeErrorBoundary
An error boundary specialized for MFEs:
<MfeErrorBoundary
name="products"
fallbackComponent={<CustomErrorUI />}
onError={(error, info) => {
// Report error to monitoring service
}}
allowRetry={true}
>
<YourMfeComponent />
</MfeErrorBoundary>MfeCommunicationProvider
The main provider that sets up communication between MFEs:
<MfeCommunicationProvider
shellName="my-app-shell"
shellVersion="1.0.0"
initialRegistry={{
// MFE configurations
}}
>
<YourAppContent />
</MfeCommunicationProvider>🪝 Hooks
useMfeEvents
Hook for event-based communication between MFEs:
const { emit, on, off, getHistory } = useMfeEvents('my-mfe');
// Emit an event
emit('user:login', { id: 123, name: 'John' });
// Listen for events
useEffect(() => {
const unsubscribe = on('cart:updated', (cartData) => {
// Handle cart update
});
return unsubscribe; // Clean up on unmount
}, []);useMfeState
Hook for shared state between MFEs (similar to React's useState):
const [value, setValue] = useMfeState('namespace', 'key', initialValue);
// Update the value (all MFEs using this state will be updated)
setValue(newValue);
// Functional updates are supported
setValue(prev => prev + 1);useMfeNavigation
Hook for navigation across MFE boundaries:
const { navigateTo, goBack, onRouteChange } = useMfeNavigation('my-mfe');
// Navigate to another route
navigateTo('/products/123');
// Listen for route changes
useEffect(() => {
const unsubscribe = onRouteChange((route) => {
console.log('Route changed:', route.pathname);
});
return unsubscribe;
}, []);useMfeContext
Hook for accessing global context shared between MFEs:
const { setContextValue, getContextValue, useContextState } = useMfeContext();
// Set a context value
setContextValue('theme', 'dark');
// Get a context value
const theme = getContextValue('theme', 'light'); // Default to 'light'
// Use context with state updates (React-like API)
const [user, setUser] = useContextState('currentUser', null);useMfeRegistry
Hook for accessing and managing the MFE registry:
const {
registerMfe,
unregisterMfe,
getMfeConfig,
isRegistered
} = useMfeRegistry();
// Register a new MFE
registerMfe({
name: 'new-mfe',
scope: 'new-mfe',
module: 'NewMfeApp'
});
// Check if an MFE is registered
if (isRegistered('products')) {
// Use the products MFE
}📡 Communication
EventBus
The core event bus for communication between MFEs:
import { EventBus } from '@unidev-hub/mfe-toolkit/communication';
// Create a new event bus
const eventBus = new EventBus();
// Add a listener
const unsubscribe = eventBus.on('user:login', (event) => {
console.log('User logged in:', event.payload);
});
// Emit an event
eventBus.emit('user:login', { id: 123, name: 'John' }, 'auth-mfe');
// Clean up
unsubscribe();SharedState
Manages shared state between MFEs:
import { SharedState } from '@unidev-hub/mfe-toolkit/communication';
import { EventBus } from '@unidev-hub/mfe-toolkit/communication';
// Create a new shared state manager
const eventBus = new EventBus();
const sharedState = new SharedState(eventBus);
// Set a value
sharedState.set('user', 'preferences', { theme: 'dark', fontSize: 'medium' });
// Get a value
const preferences = sharedState.get('user', 'preferences');
// Update a value
sharedState.update('user', 'preferences', (prev) => ({
...prev,
theme: 'light'
}));
// Clear values
sharedState.clearNamespace('user');
// Get a snapshot of all state
const snapshot = sharedState.getSnapshot();🛠️ Utils
MFE Loader
Utilities for loading MFE modules:
import { loadMfeModule, preloadMfeModule } from '@unidev-hub/mfe-toolkit/utils';
// Load a module from an MFE
loadMfeModule('products', 'ProductsApp')
.then(ProductsApp => {
// Use the loaded module
console.log('Products app loaded');
})
.catch(err => {
console.error('Failed to load Products app:', err);
});
// Preload a module for faster access later
preloadMfeModule('vendors', 'VendorsApp');MFE Registry
Utilities for managing MFE configurations:
import { mfeRegistry, initMfeRegistry } from '@unidev-hub/mfe-toolkit/utils';
// Initialize the registry
initMfeRegistry({
products: {
name: 'products',
scope: 'products',
module: 'ProductsApp',
remoteEntry: 'https://products.example.com/remoteEntry.js'
},
vendors: {
name: 'vendors',
scope: 'vendors',
module: 'VendorsApp'
}
}, true); // true to load remote entries automatically
// Register a new MFE
mfeRegistry.register({
name: 'orders',
scope: 'orders',
module: 'OrdersApp'
});
// Get an MFE configuration
const productsConfig = mfeRegistry.getConfig('products');🔍 Examples
Basic MFE Shell
A simple shell application that loads MFEs:
import React from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { MantineProvider } from '@mantine/core';
import { MfeCommunicationProvider, MfeContainer } from '@unidev-hub/mfe-toolkit';
function App() {
return (
<MantineProvider>
<BrowserRouter>
<MfeCommunicationProvider
shellName="e-commerce-shell"
shellVersion="1.0.0"
initialRegistry={{
products: {
name: 'products',
scope: 'products',
module: 'ProductsApp'
},
cart: {
name: 'cart',
scope: 'cart',
module: 'CartApp'
}
}}
>
<div className="app-shell">
<header>
<h1>E-Commerce Platform</h1>
<nav>
<a href="/">Home</a>
<a href="/products">Products</a>
<a href="/cart">Cart</a>
</nav>
</header>
<main>
<Routes>
<Route path="/" element={<HomePage />} />
<Route
path="/products/*"
element={<MfeContainer name="products" module="ProductsApp" />}
/>
<Route
path="/cart/*"
element={<MfeContainer name="cart" module="CartApp" />}
/>
</Routes>
</main>
</div>
</MfeCommunicationProvider>
</BrowserRouter>
</MantineProvider>
);
}Cross-MFE Data Sharing
Example of how to share data between MFEs:
// In Products MFE
import React from 'react';
import { useMfeEvents, useMfeState } from '@unidev-hub/mfe-toolkit';
function ProductList() {
const [products, setProducts] = useState([]);
const { emit } = useMfeEvents('products');
const [cartCount, setCartCount] = useMfeState('cart', 'itemCount', 0);
const addToCart = (product) => {
emit('cart:add-item', product);
setCartCount(prev => prev + 1);
};
return (
<div>
<h2>Products</h2>
<p>Cart Items: {cartCount}</p>
{products.map(product => (
<div key={product.id}>
<h3>{product.name}</h3>
<button onClick={() => addToCart(product)}>
Add to Cart
</button>
</div>
))}
</div>
);
}
// In Cart MFE
import React, { useEffect } from 'react';
import { useMfeEvents, useMfeState } from '@unidev-hub/mfe-toolkit';
function Cart() {
const [items, setItems] = useState([]);
const [itemCount, setItemCount] = useMfeState('cart', 'itemCount', 0);
const { on } = useMfeEvents('cart');
useEffect(() => {
const unsubscribe = on('cart:add-item', (product) => {
setItems(prev => [...prev, product]);
setItemCount(items.length + 1);
});
return unsubscribe;
}, [on, items.length]);
return (
<div>
<h2>Cart ({itemCount} items)</h2>
{items.map(item => (
<div key={item.id}>
<h3>{item.name}</h3>
<p>${item.price}</p>
</div>
))}
</div>
);
}🌐 Advanced Topics
Authentication Across MFEs
Managing authentication across MFEs:
// In Auth MFE
import { useMfeState, useMfeEvents } from '@unidev-hub/mfe-toolkit';
function AuthManager() {
const [user, setUser] = useMfeState('auth', 'currentUser', null);
const { emit } = useMfeEvents('auth');
const login = async (credentials) => {
const userData = await api.login(credentials);
setUser(userData);
emit('auth:login', userData);
};
const logout = async () => {
await api.logout();
setUser(null);
emit('auth:logout');
};
// Rest of the component
}
// In any other MFE
function ProtectedFeature() {
const [user] = useMfeState('auth', 'currentUser', null);
const { on } = useMfeEvents('app');
useEffect(() => {
const loginHandler = on('auth:login', (userData) => {
console.log('User logged in:', userData);
});
const logoutHandler = on('auth:logout', () => {
// Redirect to login page
});
return () => {
loginHandler();
logoutHandler();
};
}, [on]);
if (!user) {
return <div>Please log in to access this feature</div>;
}
return <div>Protected content</div>;
}MFE Routing Coordination
Coordinating routing across MFEs:
// In the Shell App
import { MfeCommunicationProvider, useMfeEvents } from '@unidev-hub/mfe-toolkit';
function AppShell() {
const { emit } = useMfeEvents('shell');
// Broadcast route changes
useEffect(() => {
const handleRouteChange = () => {
emit('router:change', {
pathname: window.location.pathname,
search: window.location.search,
hash: window.location.hash
});
};
window.addEventListener('popstate', handleRouteChange);
// Custom event for programmatic navigation
const originalPushState = window.history.pushState;
window.history.pushState = function() {
originalPushState.apply(this, arguments);
handleRouteChange();
};
return () => {
window.removeEventListener('popstate', handleRouteChange);
window.history.pushState = originalPushState;
};
}, [emit]);
// Rest of the shell app
}
// In an MFE
function MfeComponent() {
const { on, emit } = useMfeEvents('mfe-name');
const navigate = useNavigate();
// Listen for route changes from the shell
useEffect(() => {
const unsubscribe = on('router:change', (routeData) => {
// Handle route changes from other MFEs
// ...
});
return unsubscribe;
}, [on]);
// Notify shell about navigation
const navigateTo = (path) => {
emit('router:navigate', { path });
navigate(path);
};
return (
<div>
<button onClick={() => navigateTo('/some-path')}>Navigate</button>
</div>
);
}🤝 Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
📄 License
This project is licensed under the MIT License - see the LICENSE file for details.
Built with ❤️ by the Unidev Hub team.
