Web Components: The Secret Sauce for React Microfrontends Integration

Vysyakh AjithVysyakh Ajith
5 min read

How to break free from monolithic frontends and achieve true team independence


Microfrontends promise us the holy grail of frontend architecture: independent teams, independent deployments, and technology diversity. But there's a catch—how do you actually integrate multiple React applications without creating a maintenance nightmare?

The answer lies in an often-overlooked browser technology: Web Components. Let me show you how they solve the microfrontend puzzle elegantly.

The Microfrontend Challenge

Picture this: You have five teams building different parts of your e-commerce platform:

  • Team A: User authentication and profiles

  • Team B: Product catalog

  • Team C: Shopping cart

  • Team D: Checkout flow

  • Team E: Order history

Each team wants to use React, but they're at different versions. Team A just upgraded to React 18, while Team D is still on React 17 due to legacy dependencies. Team C wants to experiment with the latest features, but Team E needs stability.

Traditional approach: Bundle everything together, force version alignment, coordinate deployments. Result: You're back to a monolith with extra steps.

Web Components approach: Each team ships their React app wrapped in a web component. Result: True independence with seamless integration.

Web Components: The Framework-Agnostic Bridge

Web components are browser-native APIs that let you create custom HTML elements. Think of them as a universal adapter—they don't care what's inside (React, Vue, Angular, or vanilla JS), they just provide a standard interface.

Here's what makes them perfect for microfrontends:

1. True Encapsulation

class UserProfileWidget extends HTMLElement {
  connectedCallback() {
    const shadow = this.attachShadow({ mode: 'open' });
    shadow.innerHTML = `
      <style>
        /* These styles won't leak to other teams */
        .profile-card { border: 1px solid #ccc; }
      </style>
      <div class="profile-card">
        <!-- React app renders here -->
      </div>
    `;

    // Mount React app
    ReactDOM.render(<UserProfile />, shadow.querySelector('.profile-card'));
  }
}

2. Framework Independence

<!-- Shell app doesn't know or care what's inside -->
<user-profile-widget user-id="123"></user-profile-widget>
<product-catalog-widget category="electronics"></product-catalog-widget>
<shopping-cart-widget></shopping-cart-widget>

3. Clean Communication

// Microfrontend dispatches events
this.dispatchEvent(new CustomEvent('user-updated', {
  detail: { userId: 123, name: 'John Doe' },
  bubbles: true
}));

// Shell app listens
document.addEventListener('user-updated', (e) => {
  console.log('User updated:', e.detail);
});

The Implementation Blueprint

Step 1: Wrap Your React App

Each team creates a web component wrapper for their React application:

// team-a/src/UserProfileWidget.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import { UserProfileApp } from './UserProfileApp';

class UserProfileWidget extends HTMLElement {
  constructor() {
    super();
    this.root = null;
  }

  connectedCallback() {
    const mountPoint = document.createElement('div');
    this.appendChild(mountPoint);

    this.root = ReactDOM.createRoot(mountPoint);
    this.render();
  }

  disconnectedCallback() {
    if (this.root) {
      this.root.unmount();
      this.root = null;
    }
  }

  static get observedAttributes() {
    return ['user-id'];
  }

  attributeChangedCallback() {
    if (this.root) {
      this.render();
    }
  }

  render() {
    const userId = this.getAttribute('user-id');
    this.root.render(
      <UserProfileApp 
        userId={userId}
        onUserUpdate={(user) => this.handleUserUpdate(user)}
      />
    );
  }

  handleUserUpdate(user) {
    this.dispatchEvent(new CustomEvent('user-updated', {
      detail: { user },
      bubbles: true
    }));
  }
}

customElements.define('user-profile-widget', UserProfileWidget);

Step 2: Build and Deploy Independently

Each team builds their microfrontend as a standalone bundle:

// webpack.config.js
module.exports = {
  entry: './src/UserProfileWidget.js',
  output: {
    filename: 'user-profile-widget.js',
    path: path.resolve(__dirname, 'dist'),
  },
  // Configure to handle React, CSS, etc.
};

Deploy to your CDN or server:

https://team-a-cdn.com/user-profile-widget.js
https://team-b-cdn.com/product-catalog-widget.js
https://team-c-cdn.com/shopping-cart-widget.js

Step 3: The Shell App Orchestrates

The shell application loads and composes microfrontends at runtime:

// ShellApp.js
import React, { useEffect, useState } from 'react';

function ShellApp() {
  const [loadedWidgets, setLoadedWidgets] = useState(new Set());

  useEffect(() => {
    // Load microfrontends dynamically
    const widgets = [
      'https://team-a-cdn.com/user-profile-widget.js',
      'https://team-b-cdn.com/product-catalog-widget.js',
      'https://team-c-cdn.com/shopping-cart-widget.js'
    ];

    widgets.forEach(src => {
      const script = document.createElement('script');
      script.src = src;
      script.onload = () => {
        setLoadedWidgets(prev => new Set([...prev, src]));
      };
      document.head.appendChild(script);
    });

    // Global event handling
    const handleUserUpdate = (e) => {
      console.log('User updated globally:', e.detail.user);
      // Notify other widgets if needed
    };

    document.addEventListener('user-updated', handleUserUpdate);
    return () => document.removeEventListener('user-updated', handleUserUpdate);
  }, []);

  return (
    <div className="shell-app">
      <header>
        <h1>My E-commerce Platform</h1>
      </header>

      <main>
        <div className="sidebar">
          <user-profile-widget user-id="123" />
        </div>

        <div className="content">
          <product-catalog-widget category="electronics" />
        </div>

        <div className="cart">
          <shopping-cart-widget />
        </div>
      </main>
    </div>
  );
}

The Benefits in Action

Independent Deployments

Team A can push updates to their user profile widget without touching the shell app or coordinating with other teams. The shell automatically picks up the latest version.

Technology Diversity

// Team A: React 18 with Concurrent Features
<UserProfile userId={userId} />

// Team B: React 17 with Class Components  
class ProductCatalog extends Component { ... }

// Team C: Experimenting with React 19 Alpha
<ShoppingCart />

All three coexist happily in the same application.

Style Isolation

Each team's CSS stays in their lane. No more conflicts over .button classes or specificity wars.

Gradual Migration

Want to rewrite the checkout flow in Vue? No problem:

// Old React component
<checkout-widget-react />

// New Vue component (same interface)
<checkout-widget-vue />

The shell app doesn't need to change.

Real-World Considerations

Bundle Size Management

// Share common dependencies
// webpack.config.js
externals: {
  'react': 'React',
  'react-dom': 'ReactDOM'
},

// Load shared libs once in shell
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>

Error Boundaries

// Isolate failures
class MicrofrontendErrorBoundary extends React.Component {
  componentDidCatch(error, errorInfo) {
    console.error(`Microfrontend ${this.props.name} failed:`, error);
    // Report to monitoring, show fallback UI
  }

  render() {
    if (this.state.hasError) {
      return <div>Widget temporarily unavailable</div>;
    }
    return this.props.children;
  }
}

Performance Optimization

// Lazy load non-critical widgets
const LazyWidget = React.lazy(() => 
  import('./loadWidget').then(() => ({
    default: () => <non-critical-widget />
  }))
);

When NOT to Use This Approach

Web components aren't always the answer:

  • Small teams: If you have 2-3 developers, the complexity overhead isn't worth it

  • Tightly coupled features: If your "microfrontends" need to share complex state constantly

  • Performance-critical apps: The wrapper overhead might be too much

  • Server-side rendering: Web components and SSR don't play nicely (yet)

The Future is Component-Native

Web components represent a shift toward browser-native modularity. As the web platform evolves, we're seeing:

  • Better framework integration

  • Improved developer tools

  • Standardized component protocols

  • Native module loading improvements

Companies like Netflix, Spotify, and IKEA are already using variations of this approach to scale their frontend teams effectively.

Getting Started

Ready to try this approach? Here's your action plan:

  1. Start small: Pick one feature area for your first microfrontend

  2. Establish contracts: Define your event interfaces and component APIs

  3. Set up monitoring: Track loading performance and error rates

  4. Build tooling: Create templates and CI/CD pipelines for teams

  5. Document everything: Clear guidelines prevent chaos

0
Subscribe to my newsletter

Read articles from Vysyakh Ajith directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Vysyakh Ajith
Vysyakh Ajith

Aspiring full-stack developer looking forward to create end-to-end reactive web solutions with experience innovation as the motto