Docs For AI
Architecture

Micro-Frontends

Breaking monolithic frontends into independent, deployable applications

Micro-Frontends

Micro-frontends extend microservices concepts to the frontend, allowing teams to build and deploy features independently.

When to Use

Good Fit

  • Large teams working on different features
  • Features with different release cycles
  • Gradual migration from legacy systems
  • Different tech stack requirements per team
  • Small teams or simple applications
  • Tight coupling between features
  • Shared state requirements
  • Performance-critical applications

Architecture Approaches

Build-Time Integration

// Package-based micro-frontends
// Each micro-frontend is an npm package

// package.json
{
  "dependencies": {
    "@myorg/header": "^1.0.0",
    "@myorg/dashboard": "^2.0.0",
    "@myorg/settings": "^1.5.0"
  }
}

// App.tsx
import { Header } from '@myorg/header';
import { Dashboard } from '@myorg/dashboard';
import { Settings } from '@myorg/settings';

function App() {
  return (
    <>
      <Header />
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/settings" element={<Settings />} />
      </Routes>
    </>
  );
}

Runtime Integration (Module Federation)

// webpack.config.js (Host Application)
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'host',
      remotes: {
        dashboard: 'dashboard@http://localhost:3001/remoteEntry.js',
        settings: 'settings@http://localhost:3002/remoteEntry.js',
      },
      shared: {
        react: { singleton: true, requiredVersion: '^18.0.0' },
        'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
      },
    }),
  ],
};

// webpack.config.js (Dashboard Micro-frontend)
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'dashboard',
      filename: 'remoteEntry.js',
      exposes: {
        './Dashboard': './src/Dashboard',
      },
      shared: {
        react: { singleton: true },
        'react-dom': { singleton: true },
      },
    }),
  ],
};

// Host App - Dynamic import
const Dashboard = lazy(() => import('dashboard/Dashboard'));
const Settings = lazy(() => import('settings/Settings'));

function App() {
  return (
    <Suspense fallback={<Loading />}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/settings" element={<Settings />} />
      </Routes>
    </Suspense>
  );
}

Vite Module Federation

// vite.config.ts (Host)
import federation from '@originjs/vite-plugin-federation';

export default defineConfig({
  plugins: [
    react(),
    federation({
      name: 'host',
      remotes: {
        dashboard: 'http://localhost:3001/assets/remoteEntry.js',
      },
      shared: ['react', 'react-dom'],
    }),
  ],
});

// vite.config.ts (Remote)
export default defineConfig({
  plugins: [
    react(),
    federation({
      name: 'dashboard',
      filename: 'remoteEntry.js',
      exposes: {
        './Dashboard': './src/Dashboard.tsx',
      },
      shared: ['react', 'react-dom'],
    }),
  ],
});

iframe Integration

// Simple but isolated approach
function MicroFrontendIframe({ url, title }: Props) {
  const [height, setHeight] = useState(600);

  useEffect(() => {
    const handleMessage = (event: MessageEvent) => {
      if (event.data.type === 'resize') {
        setHeight(event.data.height);
      }
    };
    window.addEventListener('message', handleMessage);
    return () => window.removeEventListener('message', handleMessage);
  }, []);

  return (
    <iframe
      src={url}
      title={title}
      style={{ width: '100%', height, border: 'none' }}
    />
  );
}

// Communication via postMessage
// Child (micro-frontend)
window.parent.postMessage({ type: 'resize', height: document.body.scrollHeight }, '*');
window.parent.postMessage({ type: 'navigate', path: '/dashboard' }, '*');

// Parent (shell)
window.addEventListener('message', (event) => {
  if (event.data.type === 'navigate') {
    router.push(event.data.path);
  }
});

Web Components

// Micro-frontend as Web Component
class DashboardElement extends HTMLElement {
  connectedCallback() {
    const shadow = this.attachShadow({ mode: 'open' });
    const root = document.createElement('div');
    shadow.appendChild(root);

    // Mount React app
    ReactDOM.createRoot(root).render(<Dashboard />);
  }

  disconnectedCallback() {
    // Cleanup
  }
}

customElements.define('mf-dashboard', DashboardElement);

// Usage in any framework
<mf-dashboard user-id="123"></mf-dashboard>

Communication Patterns

Custom Events

// Dispatch from micro-frontend
function dispatchMicroFrontendEvent(type: string, detail: any) {
  window.dispatchEvent(new CustomEvent(`mf:${type}`, { detail }));
}

dispatchMicroFrontendEvent('user:updated', { userId: '123', name: 'John' });

// Listen in shell or other micro-frontend
window.addEventListener('mf:user:updated', ((event: CustomEvent) => {
  console.log('User updated:', event.detail);
}) as EventListener);

// React hook
function useMicroFrontendEvent<T>(type: string, handler: (data: T) => void) {
  useEffect(() => {
    const listener = ((event: CustomEvent<T>) => {
      handler(event.detail);
    }) as EventListener;

    window.addEventListener(`mf:${type}`, listener);
    return () => window.removeEventListener(`mf:${type}`, listener);
  }, [type, handler]);
}

Shared State

// Shared state container
class SharedState {
  private state: Record<string, any> = {};
  private listeners = new Map<string, Set<(value: any) => void>>();

  get<T>(key: string): T | undefined {
    return this.state[key];
  }

  set<T>(key: string, value: T): void {
    this.state[key] = value;
    this.listeners.get(key)?.forEach(listener => listener(value));
  }

  subscribe<T>(key: string, listener: (value: T) => void): () => void {
    if (!this.listeners.has(key)) {
      this.listeners.set(key, new Set());
    }
    this.listeners.get(key)!.add(listener);
    return () => this.listeners.get(key)?.delete(listener);
  }
}

// Expose globally
window.__SHARED_STATE__ = new SharedState();

// React hook
function useSharedState<T>(key: string): [T | undefined, (value: T) => void] {
  const [value, setValue] = useState<T | undefined>(() =>
    window.__SHARED_STATE__.get(key)
  );

  useEffect(() => {
    return window.__SHARED_STATE__.subscribe(key, setValue);
  }, [key]);

  const setSharedValue = useCallback((newValue: T) => {
    window.__SHARED_STATE__.set(key, newValue);
  }, [key]);

  return [value, setSharedValue];
}

Shared Dependencies

Dependency Strategy

// Module Federation shared config
{
  shared: {
    // Singleton - only one version loaded
    react: {
      singleton: true,
      requiredVersion: '^18.0.0',
    },
    'react-dom': {
      singleton: true,
      requiredVersion: '^18.0.0',
    },

    // Shared but allow multiple versions
    lodash: {
      singleton: false,
    },

    // Eager loading for shell dependencies
    '@myorg/design-system': {
      singleton: true,
      eager: true,
    },
  },
}

Externals CDN

<!-- Shell HTML -->
<script src="https://cdn.example.com/react@18.2.0/umd/react.production.min.js"></script>
<script src="https://cdn.example.com/react-dom@18.2.0/umd/react-dom.production.min.js"></script>
// webpack.config.js
module.exports = {
  externals: {
    react: 'React',
    'react-dom': 'ReactDOM',
  },
};

Routing

Shell-Based Routing

// Shell handles top-level routing
function Shell() {
  return (
    <BrowserRouter>
      <Header />
      <Suspense fallback={<Loading />}>
        <Routes>
          <Route path="/dashboard/*" element={<DashboardMicroFrontend />} />
          <Route path="/settings/*" element={<SettingsMicroFrontend />} />
        </Routes>
      </Suspense>
    </BrowserRouter>
  );
}

// Micro-frontend handles its own sub-routes
function DashboardMicroFrontend() {
  return (
    <Routes>
      <Route path="/" element={<DashboardHome />} />
      <Route path="/analytics" element={<Analytics />} />
      <Route path="/reports" element={<Reports />} />
    </Routes>
  );
}

Memory Router for Micro-frontends

// Micro-frontend receives basePath from shell
function MicroFrontendApp({ basePath }: { basePath: string }) {
  return (
    <MemoryRouter initialEntries={[basePath]}>
      <Routes>
        <Route path="/feature/*" element={<FeatureRoutes />} />
      </Routes>
    </MemoryRouter>
  );
}

Deployment

Independent Deployment Pipeline

# .github/workflows/deploy-dashboard.yml
name: Deploy Dashboard Micro-frontend

on:
  push:
    paths:
      - 'apps/dashboard/**'
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Build
        run: |
          cd apps/dashboard
          npm ci
          npm run build

      - name: Deploy to CDN
        run: |
          aws s3 sync dist/ s3://mf-dashboard/
          aws cloudfront create-invalidation --distribution-id ${{ secrets.CF_DIST_ID }} --paths "/*"

Version Management

// Version manifest
{
  "dashboard": {
    "url": "https://cdn.example.com/dashboard/v2.1.0/remoteEntry.js",
    "integrity": "sha384-...",
    "version": "2.1.0"
  },
  "settings": {
    "url": "https://cdn.example.com/settings/v1.5.0/remoteEntry.js",
    "integrity": "sha384-...",
    "version": "1.5.0"
  }
}

// Shell loads from manifest
async function loadMicroFrontends() {
  const manifest = await fetch('/manifest.json').then(r => r.json());

  for (const [name, config] of Object.entries(manifest)) {
    await loadRemote(config.url, { integrity: config.integrity });
  }
}

Best Practices

Micro-Frontend Guidelines

  1. Define clear boundaries between micro-frontends
  2. Minimize shared state - prefer events
  3. Use consistent design system across apps
  4. Version shared dependencies carefully
  5. Implement health checks and fallbacks
  6. Monitor performance of remote loading
  7. Test integration between micro-frontends
  8. Document communication contracts

On this page