Micro Frontends · Additional notes

1 min read
Senior4 min read
Rapid overview

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

See also