<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ Andrew Maksimchenko - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ Andrew Maksimchenko - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Sun, 24 May 2026 22:23:59 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/author/codelikeandrew/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ The Micro-Frontend Architecture Handbook ]]>
                </title>
                <description>
                    <![CDATA[ Over the years, in my role as a lead full-stack developer, solutions architect, and mentor, I’ve been immersed in the world of micro frontend architecture, working across different large-scale frontend projects where multiple teams, stacks, and deplo... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/complete-micro-frontends-guide/</link>
                <guid isPermaLink="false">6842c120a3469a9ca728862b</guid>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web Development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Frontend Development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ webdev ]]>
                    </category>
                
                    <category>
                        <![CDATA[ System Design ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Andrew Maksimchenko ]]>
                </dc:creator>
                <pubDate>Fri, 06 Jun 2025 10:21:20 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1748915817752/b35a8786-9aa7-46cd-a1d8-f82069470496.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Over the years, in my role as a lead full-stack developer, solutions architect, and mentor, I’ve been immersed in the world of micro frontend architecture, working across different large-scale frontend projects where multiple teams, stacks, and deployment pipelines had to coexist somehow.</p>
<p>As projects grew in complexity and teams worked in parallel across different stacks, it became clear that monolithic approaches couldn’t keep up. I needed practical tools that allowed easy cross-app interaction, independent deployability, better team autonomy, framework-agnosticism, and more. Some solutions worked elegantly in theory but struggled in real-world conditions. Others made things messier and more painful than helpful.</p>
<p>After diving deep into different paradigms—from iframes to Web Components, single-spa, Module Federation, Piral, Luigi, and hybrid setups—I even distilled my proven experience into a full-fledged online course on Udemy.</p>
<p>And today, in this comprehensive hands-on tutorial, I want to share my expertise and tell you more about micro-frontend architecture—method by method—with code, tradeoffs, visuals, and real-world insights.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-what-are-micro-frontends-for">What are Micro Frontends For?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-method-1-iframes-amp-cross-window-messaging">Method #1: Iframes &amp; Cross-Window Messaging</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-method-2-web-components-custom-elements-shadow-dom">Method #2: Web Components (Custom Elements + Shadow DOM)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-method-3-single-spa-the-meta-framework-approach">Method #3: Single-SPA — The Meta-Framework Approach</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-method-4-module-federation-sharing-code-at-runtime">Method #4: Module Federation - Sharing Code at Runtime</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-other-tools-amp-ecosystem-additions">Other Tools &amp; Ecosystem Additions</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-final-thoughts">Final Thoughts</a></p>
</li>
</ul>
<h2 id="heading-what-are-micro-frontends-for">What are Micro Frontends For?</h2>
<p>In traditional frontend development, we often build single, monolithic apps—one codebase, one repo, one deployment pipeline, one team. It works great for small to medium projects, sometimes even for larger ones.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748770222181/fb73c7ce-366f-4897-9ab7-b208c6e37cfa.png" alt="Monolith App Diagram - Three Features in React" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>But challenges arise when:</p>
<ul>
<li><p>Your frontend codebase expands beyond 50+ components.</p>
</li>
<li><p>Multiple development teams need autonomy over different parts and tech stacks.</p>
</li>
<li><p>Different sections require varying deployment frequencies (weekly or monthly).</p>
</li>
<li><p>You need to integrate diverse frameworks, like combining React features with an Angular-based CMS.</p>
</li>
</ul>
<p>This is where micro frontends step in.</p>
<p>Micro frontends extend the principles of microservices to the frontend world. Instead of one big frontend app, you build independent frontend modules, each owned by a team, using its own tech stack, deployed separately, and integrated at runtime.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748770253697/c78a8d84-a6a9-42af-90fd-423983c7ec77.png" alt="Micro-Frontends App Diagram - Three Apps in React, Angular, Vue" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Think of it like Lego blocks:</p>
<ul>
<li><p>Each block is similar to a self-contained micro frontend.</p>
</li>
<li><p>They plug into a shared layout or shell.</p>
</li>
<li><p>Each can evolve, update, or be replaced without affecting the others.</p>
</li>
</ul>
<p>For example, imagine that you’re building a modern e-commerce site, and here’s what your business side expects from you:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td><code>Section</code></td><td><code>Team</code></td><td><code>Stack</code></td><td><code>Deployment</code></td></tr>
</thead>
<tbody>
<tr>
<td>Product Listing</td><td>Search Team</td><td>React</td><td>Weekly</td></tr>
<tr>
<td>Product Details</td><td>Catalog Team</td><td>Angular</td><td>Monthly</td></tr>
<tr>
<td>Cart &amp; Checkout</td><td>Checkout Team</td><td>Vue</td><td>Biweekly</td></tr>
<tr>
<td>CMS Pages</td><td>Marketing Team</td><td>Vanilla JS</td><td>Daily</td></tr>
</tbody>
</table>
</div><p>Each team wants autonomy, and with micro frontends, each of these sections becomes a separate app, loaded dynamically into a shell at runtime.</p>
<h3 id="heading-why-its-getting-popular">Why It’s Getting Popular?</h3>
<p>Here are a few things everyone considers:</p>
<ol>
<li><p><strong>Independent deployments</strong> – A little or no effort to coordinate every release.</p>
</li>
<li><p><strong>Team autonomy</strong> – Teams choose their own stack and tools on the project.</p>
</li>
<li><p><strong>Incremental upgrades</strong> – Migrate legacy apps piece by piece incrementally without the need to rewrite the whole app at once.</p>
</li>
<li><p><strong>Technical agnosticism</strong> – Vue, React, Angular? Doesn’t matter. They can all work together seamlessly at the same time in a single app.</p>
</li>
<li><p><strong>Better scalability</strong> – Parallelize work across teams to enable efficiency of delivery and scale at ease.</p>
</li>
</ol>
<p>Now let’s discover how we can bring this idea to life in our projects.</p>
<p>Nowadays, there are different ways to achieve that, but not all solutions are equal. The implementation method you choose will drastically affect:</p>
<ul>
<li><p>Developer experience</p>
</li>
<li><p>Bundle sizes and performance</p>
</li>
<li><p>SEO and accessibility</p>
</li>
<li><p>Runtime stability</p>
</li>
<li><p>Interoperability across stacks</p>
</li>
</ul>
<p>So let’s begin by exploring the oldest, but still surprisingly viable method.</p>
<h2 id="heading-method-1-iframes-amp-cross-window-messaging"><strong>Method #1: Iframes &amp; Cross-Window Messaging</strong></h2>
<p>You may ask, “Aren’t iframes bad?” They’re often misunderstood. While yes, iframes can feel clunky and isolated, they’re also the most secure and decoupled way to host micro frontends—especially when you don’t trust the team on the other side.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748770863603/9daefd01-22ac-413f-bf54-c339bb6e4e9e.png" alt="Micro-Frontend Method 1 - Iframes" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<h3 id="heading-what-is-an-iframe"><strong>What Is an IFRAME?</strong></h3>
<p>An <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/iframe"><strong>iframe</strong></a> (inline frame) is an HTML element that allows you to embed another HTML page within your current webpage. The whole communication between apps is strictly based on events and delivered by means of the <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage"><strong>Post Message API.</strong></a></p>
<p>If you need to send data to another app, you simply call the <code>postMessage()</code> method on that element. On the other side, to receive a message, you just have to subscribe to the <code>message</code> event. That’s it.</p>
<h3 id="heading-real-world-example">Real-World Example</h3>
<p>Let’s see a simple example of two apps communicating with each other using <code>iframes</code> on two apps:</p>
<ul>
<li><p>The Main Web App</p>
</li>
<li><p>A Search App.</p>
</li>
</ul>
<p>Every iframe must be hosted somewhere to serve static content from it. It can be AWS Amplify, Digital Ocean, Heroku, GitHub Pages, or alike.</p>
<p>To help you out here, <a target="_blank" href="https://pages.github.com">here’s an official GitHub guideline</a> explaining how to host a website on their platform.</p>
<p>Let’s say you deployed a Search App on Github Pages and you were given this URL to host your app: <a target="_blank" href="https://search.example.com"><code>https://example.github.io</code></a>. Now let’s write some content for it.</p>
<p>Assuming that you want to post messages from the Search App to the Main Web App, and to subscribe to the incoming messages from it there. You can do it in this way:</p>
<pre><code class="lang-javascript"><span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Initializing Search App...'</span>);

<span class="hljs-comment">// Subscribe to messages from outside the iframe (like Main Web App)</span>
<span class="hljs-built_in">window</span>.addEventListener(<span class="hljs-string">'message'</span>, <span class="hljs-function">(<span class="hljs-params">event</span>) =&gt;</span> {
  <span class="hljs-keyword">if</span> (event.data?.type === <span class="hljs-string">'init'</span>) {
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Main Web App passed userId:'</span>, event.data.userId);
  }
});

<span class="hljs-comment">// Simulate sending Search results back to Main Web App</span>
<span class="hljs-built_in">window</span>.parent.postMessage({
  <span class="hljs-attr">type</span>: <span class="hljs-string">'searchResult'</span>,
  <span class="hljs-attr">payload</span>: [<span class="hljs-string">'Item A'</span>, <span class="hljs-string">'Item B'</span>]
}, <span class="hljs-string">'*'</span>);
</code></pre>
<p>Here, you initialize the search app and set up two-way communication with a parent application (such as a main web app) using the <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage"><strong>Post Message API</strong></a>. You listen for incoming messages using the built-in <code>message</code> event. Once received, that message becomes available in the <code>event.data</code> object. Finally, you simulate sending data back to the parent by posting a <code>searchResult</code> message containing a list of items. This setup enables isolated iframe-based apps to communicate safely with the main shell application.</p>
<p>Then, in the DOM of the main web app<strong>,</strong> you need to include the iframe that will render the search app, specifying the URL to the hosted search app in this way:</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">iframe</span>
  <span class="hljs-attr">id</span>=<span class="hljs-string">"search-mfe"</span>
  <span class="hljs-attr">src</span>=<span class="hljs-string">"https://example.github.io"</span>
  <span class="hljs-attr">style</span>=<span class="hljs-string">"width: 100%; height: 200px; border: none;"</span>
&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">iframe</span>&gt;</span>
</code></pre>
<p>Styles were added here to ensure that the <code>iframe</code> displays seamlessly within the layout for a cleaner UI integration.</p>
<p>And now you can pass some content from the main web app down to the search app and get some messages from it. You can accomplish it in the main web app’s JavaScript code in this way:</p>
<pre><code class="lang-javascript"><span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Initializing Main Web App...'</span>);

<span class="hljs-keyword">const</span> iframe = <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">'search-mfe'</span>);
iframe.onload = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-comment">// Send message to child iframe (inputs)</span>
  iframe.contentWindow.postMessage({ <span class="hljs-attr">type</span>: <span class="hljs-string">'init'</span>, <span class="hljs-attr">userId</span>: <span class="hljs-number">42</span> }, <span class="hljs-string">'*'</span>);
};

<span class="hljs-built_in">window</span>.addEventListener(<span class="hljs-string">'message'</span>, <span class="hljs-function">(<span class="hljs-params">event</span>) =&gt;</span> {
  <span class="hljs-comment">// Receive data from the Search App (outputs)</span>
  <span class="hljs-keyword">if</span> (event.data?.type === <span class="hljs-string">'searchResult'</span>) {
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Received result from Search App: '</span>, event.data.payload);
  }
});
</code></pre>
<p>As you see, when the <code>iframe</code> loads, the <code>init</code> event is sent to the search app (the <code>type</code> can be anything you want, just ensure it matches the one that another app expects from you). And then, in the <code>message</code> event handler as before, you can receive the incoming messages from the search app, and do something with them.</p>
<p>Here are a few pros and cons to consider, along with popular use cases:</p>
<h3 id="heading-pros"><strong>✅ Pros:</strong></h3>
<ul>
<li><p><strong>Strong sandboxing</strong>: No shared memory, no shared styles.</p>
</li>
<li><p><strong>Zero dependency clashes</strong>: One iframe is equivalent to one environment.</p>
</li>
<li><p><strong>Perfect for legacy</strong>: Easy to wrap old apps in an iframe.</p>
</li>
<li><p><strong>Practical</strong> for micro-apps in PHP, Java, Razor (ASP.NET)</p>
</li>
</ul>
<h3 id="heading-cons"><strong>❌ Cons:</strong></h3>
<ul>
<li><p>Slow rendering</p>
</li>
<li><p>Difficult shared navigation</p>
</li>
<li><p>Inconsistent/complicated styling</p>
</li>
<li><p>Complex communication</p>
</li>
<li><p>Must be hosted somewhere</p>
</li>
</ul>
<h3 id="heading-popular-use-cases"><strong>👨🏻‍💻 Popular Use Cases</strong></h3>
<ul>
<li><p>Embedding legacy dashboards (for example, old AngularJS or Java apps)</p>
</li>
<li><p>Secure cross-domain apps (for example, payments, 3rd party analytics)</p>
</li>
<li><p>Highly untrusted integrations</p>
</li>
<li><p>Embedded Ads</p>
</li>
</ul>
<p>But if you want a more fluid UX, shared components, and a smoother dev experience, you’ll want something better. That brings us to Web Components.</p>
<h2 id="heading-method-2-web-components-custom-elements-shadow-dom"><strong>Method #2: Web Components (Custom Elements + Shadow DOM)</strong></h2>
<blockquote>
<p>“What if you could ship a self-contained natively understood widget that works in any framework — React, Vue, Angular, or plain HTML?”</p>
</blockquote>
<p>That’s exactly what <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/Web_components">Web Components</a> make possible. They’re natively built into the browser as an <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/Web_components">API</a>, you don’t need a framework or extra dependency. They allow you to create reusable, scalable, encapsulated UI elements that work just like native HTML tags.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748773939725/8b017162-96a8-449d-b9b8-5fe8ef382e91.png" alt="Micro-Frontend Method 2 - Web Components" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Moreover, you can easily use them as wrappers around any elements from other UI frameworks (React, Angular, Svelte, etc) and use your framework-based components as regular native DOM elements in any web application.</p>
<p>They are, in many ways, the ideal foundation for micro frontends.</p>
<p>A web component is made of:</p>
<ul>
<li><p><strong>Custom Element</strong> - defines your own HTML tag (&lt;user-profile&gt;) and behavior</p>
</li>
<li><p><strong>Shadow DOM</strong> – provides scoped, encapsulated styles and DOM structure</p>
</li>
<li><p><strong>HTML Template</strong> – brings reusable HTML blocks/fragments</p>
</li>
<li><p><strong>Slots</strong> – acts as placeholder areas for host content (used in content projection)</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748772947093/6090d9bb-2c10-4a92-9ece-c5235b8382a2.png" alt="Micro-Frontend Method 2 - Web Components Key Blocks" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>In web components<strong>,</strong> you have to sync the data (input/output) via:</p>
<ul>
<li><p><strong>Attributes</strong> (inputs):</p>
<ul>
<li><p>In Javascript: <code>element.setAttribute()</code>, <code>element.getAttribute()</code>, and so on.</p>
</li>
<li><p>In HTML: <code>&lt;element attr1=”value1” attr2=”value2”&gt;&lt;/element&gt;</code></p>
</li>
</ul>
</li>
<li><p><strong>Properties</strong> (inputs) – <code>element.someProp = value</code> (only Javascript)</p>
</li>
<li><p><strong>Custom Events</strong> (outputs) - <code>new CustomEvent('name', data)</code></p>
</li>
</ul>
<p>First, let me show you a basic implementation of a web component, and then you’ll learn how to leverage it for micro-frontends.</p>
<p>Assuming that you’re building a reusable product-tile component that must:</p>
<ul>
<li><p>Accept one input parameter – <code>“title”</code></p>
</li>
<li><p>Send an output event <code>"add-to-cart"</code> with this <code>“title”</code> to the outside world, when the component is mounted to the DOM.</p>
</li>
</ul>
<p>Here’s how this web component could look:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// product-tile.js</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ProductTile</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">HTMLElement</span> </span>{
  <span class="hljs-comment">// Specify which attributes (inputs) to observe for changes</span>
  <span class="hljs-keyword">static</span> <span class="hljs-keyword">get</span> <span class="hljs-title">observedAttributes</span>() { <span class="hljs-keyword">return</span> [<span class="hljs-string">'title'</span>]; }

  <span class="hljs-keyword">constructor</span>() {
      <span class="hljs-built_in">super</span>(); <span class="hljs-comment">// Call base HTMLElement constructor (obligatory)</span>
      <span class="hljs-comment">// Create a Shadow DOM for style and DOM encapsulation</span>
      <span class="hljs-keyword">const</span> shadow = <span class="hljs-built_in">this</span>.attachShadow({ <span class="hljs-attr">mode</span>: <span class="hljs-string">'open'</span> });
      <span class="hljs-comment">// Populate Shadow DOM with a DIV container where React will render the player</span>
      shadow.innerHTML = <span class="hljs-string">`&lt;div id="title"&gt;&lt;/div&gt;`</span>;
  }

  <span class="hljs-comment">// Built-in Lifecycle Reaction.</span>
  <span class="hljs-comment">// Called when the custom element ProductTile is added to the DOM</span>
  connectedCallback() {
      <span class="hljs-comment">// When added to the DOM, read and render the title attribute</span>
      <span class="hljs-keyword">const</span> title = <span class="hljs-built_in">this</span>.getAttribute(<span class="hljs-string">'title'</span>) ?? <span class="hljs-string">'Unnamed Product'</span>;
      <span class="hljs-built_in">this</span>.updateTitle(title);

      <span class="hljs-comment">// Dispatch a custom event with the current title</span>
      <span class="hljs-keyword">const</span> event = <span class="hljs-keyword">new</span> CustomEvent(<span class="hljs-string">'add-to-cart'</span>, {
          <span class="hljs-attr">detail</span>: { title },
          <span class="hljs-attr">bubbles</span>: <span class="hljs-literal">true</span>,
          <span class="hljs-attr">composed</span>: <span class="hljs-literal">true</span>,
      });

      <span class="hljs-built_in">this</span>.dispatchEvent(event);
  }

  <span class="hljs-comment">// Built-in Lifecycle Reaction.</span>
  <span class="hljs-comment">// Called whenever observed attributes change.</span>
  <span class="hljs-comment">// In our case it's "title" only</span>
  attributeChangedCallback(name, oldValue, newValue) {
      <span class="hljs-keyword">if</span> (name === <span class="hljs-string">'title'</span> &amp;&amp; oldValue !== newValue) {
          <span class="hljs-built_in">this</span>.updateTitle(newValue);
      }
  }

  <span class="hljs-comment">// Internal method to safely update the title content</span>
  updateTitle(title) {
      <span class="hljs-keyword">const</span> titleElem = <span class="hljs-built_in">this</span>.shadowRoot.querySelector(<span class="hljs-string">'#title'</span>);
      titleElem.textContent = title;
  }
}

customElements.define(<span class="hljs-string">'product-tile'</span>, ProductTile);
</code></pre>
<p>Now, let me explain what’s happening here:</p>
<ul>
<li><p>First, you create a custom element class that extends from <code>HTMLElement</code> or its children. This gives you access to web component lifecycle hooks and DOM integration capabilities.</p>
</li>
<li><p>If you want to react to changes in input parameters (attributes), you have to define a static <code>observedAttributes()</code> getter that returns a list of attribute names to watch. In our case, we observe <code>“title”</code>.</p>
</li>
<li><p>Then, in the constructor:</p>
<ul>
<li><p>Call <code>super()</code> to properly inherit from <code>HTMLElement</code>.</p>
</li>
<li><p>Create a shadow DOM using <code>attachShadow({ mode: 'open' })</code>. This encapsulates your component’s internal DOM and styles. You can even use a <code>closed</code> mode here to add a higher level of isolation to the shadow DOM.</p>
</li>
<li><p>Then, populate the shadow DOM with minimal inner HTML—in this case, a <code>&lt;div&gt;</code> element that will later display the product title.</p>
</li>
</ul>
</li>
<li><p>When the component is added to the DOM, the built-in <code>connectedCallback()</code> lifecycle reaction runs:</p>
<ul>
<li><p>It reads the current value of the <code>"title"</code> attribute.</p>
</li>
<li><p>Updates the UI with an initial value in the <code>"title"</code> attribute.</p>
</li>
<li><p>Then it dispatches a custom event named <code>"add-to-cart"</code>, passing the <code>"title"</code> as detail down to it. The events are <code>bubbles: true</code> and <code>composed: true</code>, so that parent elements or host apps outside the shadow DOM can subscribe to it and catch it.</p>
</li>
</ul>
</li>
<li><p>When the title attribute changes at runtime, another built-in lifecycle reaction named <code>attributeChangedCallback()</code> runs automatically:</p>
<ul>
<li><p>It checks the new value and updates the <code>"title"</code> display accordingly.</p>
</li>
<li><p>This enables reactive behavior in the component—similar to input bindings in UI frameworks.</p>
</li>
</ul>
</li>
<li><p>Finally, you register the component globally using <code>customElements.define()</code> method (it’s available in the global <code>window</code> object), giving it:</p>
<ul>
<li><p>A tag name of <code>&lt;product-tile&gt;</code> that can be used anywhere in HTML.</p>
</li>
<li><p>A <code>reference</code> to the custom element you previously created to associate one with another.</p>
</li>
</ul>
</li>
</ul>
<p>Ultimately, here’s how you can use this component in your apps, which will work in vanilla JS, React, Angular, Svelte, Vue, whatever UI framework you choose:</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">product-tile</span> <span class="hljs-attr">title</span>=<span class="hljs-string">"Coffee Mug"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">product-tile</span>&gt;</span>
</code></pre>
<p>And then you can listen to the <code>"add-to-cart"</code> event from inside <code>ProductTile</code> component like so:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> elem = <span class="hljs-built_in">document</span>.querySelector(<span class="hljs-string">'product-tile'</span>);
elem.addEventListener(<span class="hljs-string">'add-to-cart'</span>, <span class="hljs-function"><span class="hljs-params">e</span> =&gt;</span> {
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Add to cart!'</span>, e.detail);
});
</code></pre>
<p>As you see, no <code>ReactDOM.render</code>, no <code>NgModule</code>, no extra glue. Everything is entirely native, pure <strong>JavaScript</strong> code that browsers understand.</p>
<p>And now, due to the Shadow DOM and other Web Components’ features, you can easily wrap and embed any web app written in a different framework into the Shadow Tree that will isolate your app entirely and won’t allow its layout or styles to leak out.</p>
<p>Alternatively, if you decide to publish it as a separate npm package (for example, <code>@webcomp/product-tile</code>), you can even dynamically import and mount the Web Component like so:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span>(<span class="hljs-string">'@webcomp/product-tile'</span>).then(<span class="hljs-function">() =&gt;</span> {
  <span class="hljs-comment">// Now &lt;product-tile&gt; is defined — you can create and use it</span>
  <span class="hljs-keyword">const</span> elem = <span class="hljs-built_in">document</span>.createElement(<span class="hljs-string">'product-tile'</span>);
  elem.setAttribute(<span class="hljs-string">'title'</span>, <span class="hljs-string">'Wireless Mouse'</span>);
  <span class="hljs-built_in">document</span>.body.appendChild(elem);
});
</code></pre>
<p>Or load from CDN or any hosting provider:</p>
<pre><code class="lang-jsx">&lt;script type=<span class="hljs-string">"module"</span> src=<span class="hljs-string">"https://example.github.io/product-tile.js"</span>&gt;&lt;/script&gt;
</code></pre>
<p>It’s simple, clean, and independent.</p>
<p>But you’re not here just for that, right? :) Now, let’s learn the real power of Web Components in a micro-frontends world!</p>
<h3 id="heading-micro-frontends-with-web-components"><strong>Micro-Frontends with Web Components</strong></h3>
<p>Imagine that you’ve built a Video Player in React—or perhaps want to reuse one from another team. Now the question is: How can you make this React-based player usable in any other frontend application, regardless of its underlying framework, using Web Components?</p>
<p>Let’s figure it out!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748785841227/e58d9ffd-3098-4652-ae52-a55ab218c8fd.png" alt="Micro-Frontend Method 2 - Web Components - Real World Example" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Let’s say, this video player:</p>
<ul>
<li><p>Accepts <code>src</code> and <code>controls</code> as inputs</p>
</li>
<li><p>Emits events: <code>play</code> and <code>pause</code> as outputs</p>
</li>
<li><p>Can be used in any app via <code>&lt;magic-player&gt;</code> in this way:</p>
<pre><code class="lang-xml">  <span class="hljs-tag">&lt;<span class="hljs-name">magic-player</span>
    <span class="hljs-attr">src</span>=<span class="hljs-string">"https://cdn.example.com/video.mp4"</span>
    <span class="hljs-attr">controls</span>=<span class="hljs-string">"true"</span>
  &gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">magic-player</span>&gt;</span>
</code></pre>
</li>
</ul>
<p>Now let’s get to implementation!</p>
<p><strong>🔹</strong> <strong>Step #1: Include your React player in the project</strong></p>
<p>Here, you can play around with any React component of your choice, to be honest, or you can just use a simple React Video Player like the one below:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// ReactVideoPlayer.jsx</span>

<span class="hljs-keyword">import</span> React <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">ReactVideoPlayer</span>(<span class="hljs-params">{ src, controls, onPlay, onPause }</span>) </span>{
  <span class="hljs-keyword">return</span> (
      <span class="hljs-comment">// HTML5 video element with full width and controls enabled</span>
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">video</span>
      <span class="hljs-attr">width</span>=<span class="hljs-string">"100%"</span>
      <span class="hljs-attr">controls</span>=<span class="hljs-string">{controls}</span>  {/* <span class="hljs-attr">Enable</span> / <span class="hljs-attr">Disable</span> <span class="hljs-attr">controls</span> */}
      <span class="hljs-attr">onPlay</span>=<span class="hljs-string">{onPlay}</span>      {/* <span class="hljs-attr">Callback</span> <span class="hljs-attr">for</span> <span class="hljs-attr">play</span> <span class="hljs-attr">event</span> */}
      <span class="hljs-attr">onPause</span>=<span class="hljs-string">{onPause}</span>    {/* <span class="hljs-attr">Callback</span> <span class="hljs-attr">for</span> <span class="hljs-attr">pause</span> <span class="hljs-attr">event</span> */}
    &gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">source</span> <span class="hljs-attr">src</span>=<span class="hljs-string">{src}</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"video/mp4"</span> /&gt;</span>
      Your browser does not support the video tag.
    <span class="hljs-tag">&lt;/<span class="hljs-name">video</span>&gt;</span></span>
  );
}
</code></pre>
<p><strong>🔹</strong> <strong>Step #2: Create the Web Component Wrapper</strong></p>
<p>Now, you need to create a Web Component wrapper around this React player app by mounting it into the shadow DOM of a custom element in this way:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// magic-player.element.js</span>

<span class="hljs-comment">// Define a new custom element class</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MagicPlayerElement</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">HTMLElement</span> </span>{
  <span class="hljs-keyword">constructor</span>() {
    <span class="hljs-built_in">super</span>(); <span class="hljs-comment">// Call base HTMLElement constructor (obligatory)</span>

    <span class="hljs-comment">// Create a Shadow DOM for style and DOM encapsulation</span>
    <span class="hljs-keyword">const</span> shadowRoot = <span class="hljs-built_in">this</span>.attachShadow({ <span class="hljs-attr">mode</span>: <span class="hljs-string">'open'</span> });
    <span class="hljs-comment">// Populate Shadow DOM with a DIV container where React will render the player</span>
    shadowRoot.innerHTML = <span class="hljs-string">`
        &lt;div id="react-video-player"&gt;&lt;/div&gt;
    `</span>;
  }
}

customElements.define(<span class="hljs-string">'magic-player'</span>, MagicPlayerElement);
</code></pre>
<p>Then you need to add inputs and outputs like so:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// magic-player.element.js</span>

<span class="hljs-comment">// Define a new custom element class</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MagicPlayerElement</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">HTMLElement</span> </span>{
  <span class="hljs-comment">// Specify which attributes (inputs) to observe for changes</span>
  <span class="hljs-keyword">static</span> <span class="hljs-keyword">get</span> <span class="hljs-title">observedAttributes</span>() { <span class="hljs-keyword">return</span> [<span class="hljs-string">'src'</span>, <span class="hljs-string">'controls'</span>]; }

  <span class="hljs-keyword">constructor</span>() {
    <span class="hljs-built_in">super</span>(); <span class="hljs-comment">// Call base HTMLElement constructor (obligatory)</span>

    <span class="hljs-comment">// Create a Shadow DOM for style and DOM encapsulation</span>
    <span class="hljs-keyword">const</span> shadowRoot = <span class="hljs-built_in">this</span>.attachShadow({ <span class="hljs-attr">mode</span>: <span class="hljs-string">'open'</span> });
    <span class="hljs-comment">// Populate Shadow DOM with a DIV container where React will render the player</span>
    shadowRoot.innerHTML = <span class="hljs-string">`
        &lt;div id="react-video-player"&gt;&lt;/div&gt;
    `</span>;
  }

  <span class="hljs-comment">// Helper-like method to dispatch native-like events (our outputs)</span>
  <span class="hljs-comment">// In our case, it will be triggered for "onPlay" and "onPause" events</span>
  dispatch(eventName, detail = {}) {
      <span class="hljs-keyword">const</span> event = <span class="hljs-keyword">new</span> CustomEvent(eventName, {
      detail,            <span class="hljs-comment">// Pass custom data ("onPlay" or "onPause")</span>
      <span class="hljs-attr">bubbles</span>: <span class="hljs-literal">true</span>,     <span class="hljs-comment">// Allow event to bubble up</span>
      <span class="hljs-attr">composed</span>: <span class="hljs-literal">true</span>     <span class="hljs-comment">// Allow it to cross the Shadow DOM boundary</span>
    });
    <span class="hljs-built_in">this</span>.dispatchEvent(event);
  }
}

customElements.define(<span class="hljs-string">'magic-player'</span>, MagicPlayerElement);
</code></pre>
<p>And lastly, add two built-in lifecycle reactions to render a React video player app when the page loads and every time the inputs change:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// magic-player.element.jsx</span>

<span class="hljs-comment">// Define a new custom element class</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MagicPlayerElement</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">HTMLElement</span> </span>{
  <span class="hljs-comment">// Specify which attributes (inputs) to observe for changes</span>
  <span class="hljs-keyword">static</span> <span class="hljs-keyword">get</span> <span class="hljs-title">observedAttributes</span>() { <span class="hljs-keyword">return</span> [<span class="hljs-string">'src'</span>, <span class="hljs-string">'controls'</span>]; }

  <span class="hljs-keyword">constructor</span>() {
    <span class="hljs-built_in">super</span>(); <span class="hljs-comment">// Call base HTMLElement constructor (obligatory)</span>

    <span class="hljs-comment">// Create a Shadow DOM for style and DOM encapsulation</span>
    <span class="hljs-keyword">const</span> shadow = <span class="hljs-built_in">this</span>.attachShadow({ <span class="hljs-attr">mode</span>: <span class="hljs-string">'open'</span> });
    <span class="hljs-comment">// Populate Shadow DOM with a DIV container where React will render the player</span>
    shadow.innerHTML = <span class="hljs-string">`
        &lt;div id="react-video-player"&gt;&lt;/div&gt;
    `</span>;
  }

  <span class="hljs-comment">// Helper-like method to dispatch native-like events (our outputs)</span>
  <span class="hljs-comment">// In our case, it will be triggered for "onPlay" and "onPause" events</span>
  dispatch(eventName, detail = {}) {
      <span class="hljs-keyword">const</span> event = <span class="hljs-keyword">new</span> CustomEvent(eventName, {
      detail,            <span class="hljs-comment">// Pass custom data ("onPlay" or "onPause")</span>
      <span class="hljs-attr">bubbles</span>: <span class="hljs-literal">true</span>,     <span class="hljs-comment">// Allow event to bubble up</span>
      <span class="hljs-attr">composed</span>: <span class="hljs-literal">true</span>     <span class="hljs-comment">// Allow it to cross the Shadow DOM boundary</span>
    });
    <span class="hljs-built_in">this</span>.dispatchEvent(event);
  }

  <span class="hljs-comment">// Built-in Lifecycle Reaction.</span>
  <span class="hljs-comment">// Called when the custom element &lt;magic-player&gt; is added to the DOM</span>
  connectedCallback() {
    <span class="hljs-built_in">this</span>.render();
  }

  <span class="hljs-comment">// Built-in Lifecycle Reaction.</span>
  <span class="hljs-comment">// Called whenever observed attributes change.</span>
  <span class="hljs-comment">// In our case it's "src" and "controls"</span>
  attributeChangedCallback() {
    <span class="hljs-built_in">this</span>.render();
  }

  <span class="hljs-comment">// Render the React player inside the container</span>
  render() {
    <span class="hljs-keyword">const</span> src = <span class="hljs-built_in">this</span>.getAttribute(<span class="hljs-string">'src'</span>);
    <span class="hljs-keyword">const</span> controls = <span class="hljs-built_in">this</span>.getAttribute(<span class="hljs-string">'controls'</span>) === <span class="hljs-string">'true'</span>;
    <span class="hljs-keyword">const</span> mount = <span class="hljs-built_in">this</span>.shadowRoot.querySelector(<span class="hljs-string">'#react-video-player'</span>);

    ReactDOM.createRoot(mount).render(
      <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">ReactVideoPlayer</span>
        <span class="hljs-attr">src</span>=<span class="hljs-string">{src}</span>
        <span class="hljs-attr">controls</span>=<span class="hljs-string">{controls}</span>
        <span class="hljs-attr">onPlay</span>=<span class="hljs-string">{()</span> =&gt;</span> this.dispatch('play')}
        onPause={() =&gt; this.dispatch('pause')}
      /&gt;</span>
    );
  }
}

customElements.define(<span class="hljs-string">'magic-player'</span>, MagicPlayerElement);
</code></pre>
<p><strong>🔹</strong> <strong>Step #3: Connect your React-Player to any UI framework:</strong></p>
<p>Then, in the main web app (whatever UI framework you’re using there). We put our newly created React video player wrapper in any place in the DOM, passing down initial attributes (inputs) to it:</p>
<pre><code class="lang-xml"><span class="hljs-comment">&lt;!-- Use your new React-based player anywhere! --&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">magic-player</span>
  <span class="hljs-attr">src</span>=<span class="hljs-string">"https://cdn.example.com/movie.mp4"</span>
  <span class="hljs-attr">controls</span>=<span class="hljs-string">"true"</span>
&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">magic-player</span>&gt;</span>
</code></pre>
<p>And then you can easily subscribe to the custom events (outputs) from inside the React app:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Listen to native-style events from the custom element</span>
<span class="hljs-keyword">const</span> magicPlayer = <span class="hljs-built_in">document</span>.querySelector(<span class="hljs-string">'magic-player'</span>);
magicPlayer.addEventListener(<span class="hljs-string">'play'</span>, <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Video has started playing!'</span>);
});

magicPlayer.addEventListener(<span class="hljs-string">'pause'</span>, <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Video has been paused.'</span>);
});
</code></pre>
<p>That’s it! Now, try to accomplish the same with a different <strong>UI framework</strong>!</p>
<h3 id="heading-pros-1"><strong>✅ Pros</strong></h3>
<ul>
<li><p><strong>Framework-agnostic:</strong> Works in React, Angular, Vue, Svelte, or even plain HTML — no rewrites needed</p>
</li>
<li><p><strong>Natively supported by browsers:</strong> No need for external libraries or frameworks — just HTML, JS, and CSS.</p>
</li>
<li><p>No extra configuration or hosting needed as in iframes. But still, components can be published to npm/CDNs and reused across multiple apps.</p>
</li>
<li><p><strong>Intuitive &amp; easy communication:</strong> Expose native DOM attributes as inputs and native custom events as outputs.</p>
</li>
<li><p><strong>SSR-friendly with hydration:</strong> It supports serialization, declarative shadow DOM, and can be server-rendered and hydrated, especially using modern tools.</p>
</li>
<li><p><strong>Supports Accessibility</strong> (ARIA attributes and roles).</p>
</li>
</ul>
<h3 id="heading-cons-1"><strong>❌ Cons</strong></h3>
<ul>
<li><p><strong>Integration Difficulties</strong>: If you want to bridge two apps in different technical stacks, you need to properly manage their communication in a custom element wrapper and its shadow DOM.</p>
</li>
<li><p><strong>Limited Support for old Browsers</strong>: If you need compatibility with legacy browsers like Internet Explorer 10, Web Components need a polyfill. But here’s a popular repository with all polyfills for Web Components: <a target="_blank" href="https://github.com/webcomponents/polyfills">https://github.com/webcomponents/polyfills</a></p>
</li>
<li><p><strong>Global State Isolation</strong>: There’s no built-in way to share state across components. You’ll need to implement your own global bus or event bridge using <code>CustomEvents</code> or alike.</p>
</li>
</ul>
<h3 id="heading-popular-use-cases-1"><strong>👨🏻‍💻 Popular Use Cases</strong></h3>
<ul>
<li><p>Reusable Design systems &amp; UI libraries</p>
</li>
<li><p>Micro frontends inside framework apps</p>
</li>
<li><p>Legacy integration to modern stack and vice versa</p>
</li>
<li><p>Cross-team component delivery</p>
</li>
<li><p>CDN-based plug-and-play UIs</p>
</li>
</ul>
<p>The Web Components API has many more possibilities and power. So, if you want, you can go deeper and advance your knowledge by passing any available free course on freeCodeCamp or passing the one I’ve built myself around this technique on Udemy.</p>
<p>Now let’s move on!</p>
<h2 id="heading-method-3-single-spa-the-meta-framework-approach"><strong>Method #3: Single</strong>-SPA — The Meta-<strong>Framework Approach</strong></h2>
<blockquote>
<p>“What if instead of embedding micro frontends as Web Components or iframes, we had a system that orchestrated multiple SPAs together in one layout?”</p>
</blockquote>
<p>That’s what <a target="_blank" href="https://single-spa.js.org/">single-spa</a> is all about. It’s not a rendering library, it’s a runtime JavaScript router and orchestrator for micro frontends.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748788736898/90800e32-f8d0-4fc5-aedb-e7ce8d753c4c.png" alt="Micro-Frontend Method 3 - Single SPA" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<blockquote>
<p><em>Source:</em> <a target="_blank" href="https://single-spa.js.org/">https://single-spa.js.org</a></p>
</blockquote>
<h3 id="heading-what-is-single-spa"><strong>What Is</strong> single-spa<strong>?</strong></h3>
<p>single-spa (Single Page Application) lets you build and run multiple independent SPAs (React, Vue, Angular, and so on) inside one webpage. Each SPA is responsible for part of the UI and is loaded dynamically depending on the current route.</p>
<p>In short, it’s a <strong>framework</strong> that:</p>
<ul>
<li><p>Loads your micro frontends when needed</p>
</li>
<li><p>Mounts/unmounts them cleanly</p>
</li>
<li><p>Coordinates routing and lifecycles</p>
</li>
<li><p>Supports different frameworks in the same app.</p>
</li>
</ul>
<h3 id="heading-real-life-example"><strong>Real-Life Example</strong></h3>
<p>Let’s say you have this route breakdown:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td><code>Path</code></td><td><code>Micro Frontend App</code></td><td><code>Stack</code></td><td><code>App Name</code></td></tr>
</thead>
<tbody>
<tr>
<td>/products</td><td>Product Listing App</td><td>React</td><td><code>@shop/products</code></td></tr>
<tr>
<td>/checkout</td><td>Checkout App</td><td>Vue</td><td><code>@shop/checkout</code></td></tr>
<tr>
<td>/account</td><td>Account Dashboard</td><td>Angular</td><td><code>@shop/account</code></td></tr>
</tbody>
</table>
</div><p>Each one is a <strong>fully independent SPA</strong>, and single-spa loads them as needed.</p>
<p><strong>🔹</strong> <strong>Step #1:</strong> single-spa <strong>installation</strong></p>
<p>First, you need to install the single-spa as a dependency for your project:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Create a new project (if it's not yet)</span>
npm init

<span class="hljs-comment"># Install Single SPA</span>
npm install single-spa systemjs
</code></pre>
<p>Notice that we also installed the <code>systemjs</code> package. This package is responsible for the dynamic runtime module loading that makes Single-SPA work seamlessly. It uses <code>SystemJS</code> as a module loader to allow micro frontends to be:</p>
<ol>
<li><p>Loaded at runtime</p>
</li>
<li><p>Independently deployed</p>
</li>
<li><p>Framework-agnostic</p>
</li>
<li><p>Lazy-loaded only when needed</p>
</li>
</ol>
<p>Now you need to implement each micro-app. For instance, let’s see how the <code>@shop/products</code> app written in React could be managed.</p>
<p><strong>🔹 Step #2: Project Structure</strong></p>
<p>The project structure for each micro app can look like this:</p>
<pre><code class="lang-apache"><span class="hljs-attribute">shop</span>/products/
├── <span class="hljs-attribute">src</span>/
│   ├── <span class="hljs-attribute">root</span>.component.jsx
│   └── <span class="hljs-attribute">index</span>.single-spa.js
├── <span class="hljs-attribute">public</span>/
│   └── <span class="hljs-attribute">index</span>.html
├── <span class="hljs-attribute">package</span>.json
└── <span class="hljs-attribute">webpack</span>.config.js
</code></pre>
<p><strong>🔹 Step #3: Root Micro App Component</strong></p>
<p>The <code>root.component.jsx</code> file represents the root of the React app that will be mounted to the main DOM using single-spa. Here’s a simple example:</p>
<pre><code class="lang-jsx"><span class="hljs-comment">// src/root.component.jsx</span>
<span class="hljs-keyword">import</span> React <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Root</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">padding:</span> '<span class="hljs-attr">1rem</span>', <span class="hljs-attr">border:</span> '<span class="hljs-attr">1px</span> <span class="hljs-attr">solid</span> #<span class="hljs-attr">ccc</span>' }}&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">h2</span>&gt;</span>🛍 Product Micro App<span class="hljs-tag">&lt;/<span class="hljs-name">h2</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>This is a micro frontend powered by React + Single-SPA!<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
  );
}
</code></pre>
<p><strong>🔹</strong> <strong>Step #4: Set Up Lifecycle Hooks</strong></p>
<p>Also, each Micro App in single-spa requires an entry point with at least three core functions/lifecycle hooks. For that purpose, you will need a separate file, which you can name as <code>index.single-spa.js</code> and it will provide the implementation of those hooks, like:</p>
<ul>
<li><p><code>bootstrap()</code> - Called when the micro app is launched by the main app (Shell) before mounting to the DOM</p>
</li>
<li><p><code>mount()</code> - Called when the app is attached to the host in the DOM</p>
</li>
<li><p><code>unmount()</code> - Called when the app is removed/detached from the DOM</p>
</li>
</ul>
<p>And here’s an example of what they could look like:</p>
<pre><code class="lang-jsx"><span class="hljs-comment">// src/index.single-spa.js</span>

<span class="hljs-keyword">import</span> React <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;
<span class="hljs-keyword">import</span> ReactDOM <span class="hljs-keyword">from</span> <span class="hljs-string">'react-dom/client'</span>;
<span class="hljs-keyword">import</span> Root <span class="hljs-keyword">from</span> <span class="hljs-string">'./root.component.jsx'</span>;

<span class="hljs-comment">// Hold the React root instance for reuse</span>
<span class="hljs-keyword">let</span> root = <span class="hljs-literal">null</span>;

<span class="hljs-comment">// Called once when the micro frontend is first initialized</span>
<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">bootstrap</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">return</span> <span class="hljs-built_in">Promise</span>.resolve();
}

<span class="hljs-comment">// Called every time the route matches and the app should appear</span>
<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">mount</span>(<span class="hljs-params">props</span>) </span>{
  <span class="hljs-keyword">return</span> <span class="hljs-built_in">Promise</span>.resolve().then(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> container = <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">'product-container'</span>) || createContainer();
    root = ReactDOM.createRoot(container);
    root.render(<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Root</span> /&gt;</span></span>);
  });
}

<span class="hljs-comment">// Called when the route no longer matches (cleanup)</span>
<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">unmount</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">return</span> <span class="hljs-built_in">Promise</span>.resolve().then(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">if</span> (root) {
      root.unmount();
    }
  });
}

<span class="hljs-comment">// Create a container div if it doesn't exist</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">createContainer</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> div = <span class="hljs-built_in">document</span>.createElement(<span class="hljs-string">'div'</span>);
  div.id = <span class="hljs-string">'product-container'</span>;
  <span class="hljs-built_in">document</span>.body.appendChild(div);
  <span class="hljs-keyword">return</span> div;
}
</code></pre>
<p>As you see, you have to resolve a Promise in all lifecycle hooks and ensure the React app is mounted and unmounted properly based on the React best practices.</p>
<p><strong>🔹</strong> <strong>Step #5: Configuring Webpack for SystemJS</strong></p>
<p>Also, each micro-app in single-spa needs a separate configuration. For that, you will include a <code>webpack.config.js</code> file, specifying how to build the app (<code>output</code>), where to host it (<code>publicPath</code>), and so on.</p>
<p>Since single-spa uses the <code>SystemJS</code> package, the <code>libraryTarget</code> will be <code>system</code> for all micro apps.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// webpack.config.js</span>
<span class="hljs-built_in">module</span>.exports = {
  <span class="hljs-attr">externals</span>: {
    <span class="hljs-attr">react</span>: <span class="hljs-string">'React'</span>,
    <span class="hljs-string">'react-dom'</span>: <span class="hljs-string">'ReactDOM'</span>,
  },
  <span class="hljs-attr">output</span>: {
    <span class="hljs-attr">filename</span>: <span class="hljs-string">'products.js'</span>,
    <span class="hljs-attr">libraryTarget</span>: <span class="hljs-string">'system'</span>, <span class="hljs-comment">// SystemJS-compatible format</span>
    <span class="hljs-attr">publicPath</span>: <span class="hljs-string">'http://localhost:8500/'</span>, <span class="hljs-comment">// Host location of this micro app</span>
  },
};
</code></pre>
<p>This app will be hosted on the <a target="_blank" href="http://localhost:8500"><code>localhost:8500</code></a>. For production, you will have to use any suitable hosting provider (like the ones described in the iframes section).</p>
<p><strong>🔹</strong> <strong>Step #6: Registering the Micro App in Root-Config</strong></p>
<p>Next, it’s time to register a new micro-app in the Singla-SPA root config. Here’s how you can do it:</p>
<p>Create a <code>root-config.js</code> file in the root of the project and fill it with this content:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// root-config.js (host shell)</span>
<span class="hljs-keyword">import</span> { registerApplication, start } <span class="hljs-keyword">from</span> <span class="hljs-string">'single-spa'</span>;

registerApplication({
  <span class="hljs-attr">name</span>: <span class="hljs-string">'@shop/products'</span>,
  <span class="hljs-attr">app</span>: <span class="hljs-function">() =&gt;</span> System.import(<span class="hljs-string">'@shop/products'</span>),
  <span class="hljs-attr">activeWhen</span>: [<span class="hljs-string">'/products'</span>],
});

start(); <span class="hljs-comment">// Initializes routing and micro app lifecycles</span>
</code></pre>
<p>First, you have to register the application, and then you start it to enable routing and the micro app lifecycle. The registration for other micro apps will look the same.</p>
<p><strong>Note</strong>: <code>System.import()</code> is part of <code>SystemJS</code>, used by default in single-spa for loading remote apps.</p>
<p>Also, single-spa comes with so-called "Parcels" – a lower-level construct in comparison to applications. They’re essentially self-contained pieces of UI that you can dynamically mount anywhere. Think of them like “mini microfrontends” or reusable widgets that don’t control routing:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Example</span>
mountParcel(SomeParcelComponent, { <span class="hljs-attr">domElement</span>: <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">'micro-app'</span>) });
</code></pre>
<p>You’d use them when:</p>
<ul>
<li><p>You don’t want the parcel to own a route.</p>
</li>
<li><p>You need to inject a micro frontend dynamically inside another one.</p>
</li>
<li><p>You want encapsulated logic (like a widget) embedded within a larger app.</p>
</li>
</ul>
<p>In all other cases, prefer the usage of a <code>registerApplication(...)</code> function.</p>
<p><strong>🔹</strong> <strong>Step #7: Adding Micro App to SystemJS Import Map</strong></p>
<p>The last step is to register the micro app in <code>SystemJS</code>. For that, in your root <code>index.html</code> file, you need to add the following two scripts:</p>
<pre><code class="lang-xml"><span class="hljs-comment">&lt;!-- public/index.html --&gt;</span>

<span class="hljs-meta">&lt;!DOCTYPE <span class="hljs-meta-keyword">html</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">html</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"en"</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span> <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>Micro Frontend Shell<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span> <span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">nav</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"/products"</span>&gt;</span>Products<span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span> |
    <span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"/checkout"</span>&gt;</span>Checkout<span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">nav</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- Import maps handled by bundler or injected at runtime --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"systemjs-importmap"</span>&gt;</span><span class="javascript">
    {
      <span class="hljs-string">"imports"</span>: {
        <span class="hljs-string">"@shop/root-config"</span>: <span class="hljs-string">"http://localhost:9000/root-config.js"</span>,
        <span class="hljs-string">"@shop/products"</span>: <span class="hljs-string">"http://localhost:8500/products.js"</span>,
        <span class="hljs-comment">// other micro apps</span>
      }
    }
  </span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>

  <span class="hljs-comment">&lt;!-- Start the root-config application --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
    System.import(<span class="hljs-string">'@shop/root-config'</span>);
  </span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span>
</code></pre>
<p>First, you have to add a script with an import map declaration. As you see, it represents a JSON where:</p>
<ul>
<li><p>Each key is the micro app name and</p>
</li>
<li><p>Each value is the URL where the main JS file (from the bundle) actually lives</p>
</li>
</ul>
<p>Note that we’ve added the <code>@shop/root-config</code> here to the import map to tell <code>SystemJS</code> where to fetch the main JavaScript file for the main/shell app so it knows how to resolve and execute <code>System.import('@shop/root-config')</code> properly.</p>
<p>Secondly, you include another script to start the main / shell application. It executes the JS file you just mapped in the import map above. Treat it as the real “boot” of your shell app:</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
  System.import(<span class="hljs-string">'@shop/root-config'</span>);
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
</code></pre>
<p>That’s it! Now go ahead and try doing the same with other micro-apps in Vue (Checkout App) and Angular (Account Dashboard).</p>
<p>Here’s a simple diagram illustrating this connection:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748789553598/4729600f-54d7-4d72-97e7-462093cf08b5.png" alt="Micro-Frontend Method 3 - Single SPA - Real World Example" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Now that you’ve registered and integrated your first micro app, you might be wondering if this approach right for you. Let’s quickly look at the benefits and limitations of using single-spa in production.</p>
<h3 id="heading-pros-2"><strong>✅ Pros</strong></h3>
<ul>
<li><p><strong>Built-in Routing &amp; Lifecycles</strong> - No need to reinvent navigation or mounting logic</p>
</li>
<li><p><strong>Cross-framework support</strong> - React, Vue, Angular can all co-exist</p>
</li>
<li><p><strong>Fine-grained loading</strong> - Only load the active app (lazy and efficient)</p>
</li>
<li><p><strong>Flexible project structure</strong> - can be monorepo or polyrepo</p>
</li>
<li><p><strong>Good CLI tooling -</strong> create and link MFEs with create-single-spa &amp; helpers</p>
</li>
</ul>
<h3 id="heading-cons-2"><strong>❌ Cons</strong></h3>
<ul>
<li><p><strong>Complex learning curve</strong> - Lifecycle APIs and <code>SystemJS</code> can be intimidating</p>
</li>
<li><p><strong>Configurations</strong> <strong>can get verbose</strong> – Managing multiple registries, import maps, deployment URLs, and lifecycle wrappers across apps adds setup overhead</p>
</li>
<li><p><strong>Shared state is manual</strong> - You must implement custom global state solutions</p>
</li>
<li><p><strong>Hard to SSR</strong> - Designed for full client-side rendering</p>
</li>
<li><p><strong>More boilerplate</strong> - Each app needs wrappers for lifecycles, routing, and so on.</p>
</li>
<li><p><strong>Global styles leak -</strong> No default encapsulation like Shadow DOM</p>
</li>
</ul>
<p>And a few popular <strong>use cases</strong> for it:</p>
<h3 id="heading-popular-use-cases-2"><strong>👨🏻‍💻 Popular Use Cases</strong></h3>
<p>You can use single-spa when:</p>
<ul>
<li><p>You want a central router managing all micro frontends</p>
</li>
<li><p>Teams are using different frameworks</p>
</li>
<li><p>You prefer full SPA experiences over isolated widgets</p>
</li>
<li><p>You don’t mind some boilerplate for orchestration</p>
</li>
<li><p>You’re okay with a purely client-side setup</p>
</li>
</ul>
<p>Let’s move on!</p>
<h2 id="heading-method-4-module-federation-sharing-code-at-runtime"><strong>Method #4: Module Federation - Sharing Code at Runtime</strong></h2>
<blockquote>
<p>“What if your micro frontends could load each other’s components, modules, or libraries at runtime — without iframes, without import maps, and without repackaging?”</p>
</blockquote>
<p>That’s exactly what <a target="_blank" href="https://module-federation.io/">Module Federation</a>, introduced in <a target="_blank" href="https://webpack.js.org/blog/2020-10-10-webpack-5-release/">Webpack 5</a>, makes possible. It’s fairly new and it allows multiple, separately built and deployed applications to share modules in real-time, via the browser.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748789750463/ad976d48-f564-4e94-a3ca-c18e9612dc55.png" alt="ad976d48-f564-4e94-a3ca-c18e9612dc55" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<blockquote>
<p><em>Source:</em> <a target="_blank" href="https://module-federation.io/">https://module-federation.io/</a></p>
</blockquote>
<p>With Module Federation, you can:</p>
<ul>
<li><p>Import components across independent builds</p>
</li>
<li><p>Share React, Vue, or any dependency</p>
</li>
<li><p>Version-control exposed modules</p>
</li>
<li><p>Ship independently, yet consume each other</p>
</li>
</ul>
<p>Module Federation is what makes micro frontends in a single cohesive layout truly feel like one app.</p>
<p>Now let’s see it in action!</p>
<h3 id="heading-real-life-example-1"><strong>Real-Life Example</strong></h3>
<p>Let’s assume that you have to build two self-contained apps:</p>
<ul>
<li><p>Main / Host app (shell) — loads components from others (let’s say it’s in React)</p>
</li>
<li><p>Remote app (product-app) — exposes components written also in React to others</p>
</li>
</ul>
<p>Module Federation allows you to export these components without publishing them to NPM or wrapping them as a Web Component. Instead, the host app will load the component directly at runtime from the compiled JavaScript bundle.</p>
<p>Here’s how the project structure could look:</p>
<p><strong>Product App:</strong></p>
<pre><code class="lang-apache"><span class="hljs-attribute">product</span>-app/                ← Remote Micro Frontend
├── <span class="hljs-attribute">public</span>/
│   └── <span class="hljs-attribute">index</span>.html          ← Mount point for optional local test render
├── <span class="hljs-attribute">src</span>/
│   ├── <span class="hljs-attribute">ProductTile</span>.jsx     ← Component to expose
│   └── <span class="hljs-attribute">index</span>.js            ← Optional: local entry point
├── <span class="hljs-attribute">webpack</span>.config.js       ← Exposes Product App
├── <span class="hljs-attribute">package</span>.json
└── .<span class="hljs-attribute">babelrc</span> / .gitignore / etc
</code></pre>
<p>Note, that <code>webpack.config.js</code> must be at the root level, same as <code>package.json</code>, so <code>Webpack</code> can locate it automatically.</p>
<p><strong>Main / Host App (shell):</strong></p>
<pre><code class="lang-apache"><span class="hljs-attribute">host</span>-app/                     
├── <span class="hljs-attribute">public</span>/
│   └── <span class="hljs-attribute">index</span>.html        ← Mount point
├── <span class="hljs-attribute">src</span>/
│   ├── <span class="hljs-attribute">App</span>.jsx           ← Mounts ProductTile from remote
│   └── <span class="hljs-attribute">bootstrap</span>.js      ← App entry point
├── <span class="hljs-attribute">webpack</span>.config.js     ← Loads remotes via Module Federation
└── <span class="hljs-attribute">package</span>.json
</code></pre>
<p>You can keep them both in a monorepo or host them in entirely different repos.</p>
<p>🔹 <strong>Step #0: Initiate projects (Host + Product Apps)</strong></p>
<p>If you know how to do it, you can set up two separate React applications yourself for the Host App and one for the Remote (Product App), or initialize them in this way:</p>
<pre><code class="lang-bash">npm init
npm install react react-dom
</code></pre>
<p><strong>🔹</strong> <strong>Step #1: Install Webpack 5 + dependencies (Host + Product Apps)</strong></p>
<p>Before you do anything federation-related, both the host and remote apps must be set up with Webpack 5 and its plugins. Go ahead and run this in both projects:</p>
<pre><code class="lang-bash">npm install webpack webpack-cli webpack-dev-server html-webpack-plugin --save-dev
</code></pre>
<p>A few notes about these packages:</p>
<ul>
<li><p><code>webpack + webpack-cli</code> — Core bundler and CLI</p>
</li>
<li><p><code>webpack-dev-server</code> — Local server for hot reload + module exposure</p>
</li>
<li><p><code>html-webpack-plugin</code> — Automatically injects your bundles into HTML</p>
</li>
<li><p>Optional but common: You can add <code>Babel</code>, <code>React preset</code>, <code>loaders</code>, and so on, for <code>JSX</code>/<code>TSX</code> support later.</p>
</li>
</ul>
<p>This setup gives you a foundation. From here, you can add module federation to connect apps together.</p>
<p><strong>🔹</strong> <strong>Step #2: Create the Remote App (Product App)</strong></p>
<p>Let’s start with the remote app, the one exposing a React component to be consumed by others.</p>
<p>Here’s a simple <code>ProductTile</code> React component (of course, you can implement yours):</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// product-app/src/ProductTile.jsx</span>

<span class="hljs-keyword">import</span> React <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">ProductTile</span>(<span class="hljs-params">{ title }</span>) </span>{
  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">border:</span> '<span class="hljs-attr">1px</span> <span class="hljs-attr">solid</span> #<span class="hljs-attr">aaa</span>', <span class="hljs-attr">padding:</span> '<span class="hljs-attr">1rem</span>' }}&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">h3</span>&gt;</span>🛍 {title}<span class="hljs-tag">&lt;/<span class="hljs-name">h3</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
  );
}
</code></pre>
<p>A <code>ProductTile</code> component supplies a prop – <code>“title”</code> – and renders it.</p>
<p>Now let’s expose this component to other apps, not just render it locally.</p>
<p><strong>🔹</strong> <strong>Step #3: Configure Webpack in the Remote App (Product App)</strong></p>
<p>This will be done utilizing module federation, which you must enable in <code>webpack.config.js</code> file. Here’s how it can be done. At the very top of the file, you will need to import these packages:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// product-app/webpack.config.js</span>

<span class="hljs-keyword">const</span> HtmlWebpackPlugin = <span class="hljs-built_in">require</span>(<span class="hljs-string">'html-webpack-plugin'</span>);
<span class="hljs-keyword">const</span> ModuleFederationPlugin = <span class="hljs-built_in">require</span>(<span class="hljs-string">'webpack'</span>).container.ModuleFederationPlugin;
<span class="hljs-keyword">const</span> path = <span class="hljs-built_in">require</span>(<span class="hljs-string">'path'</span>);
</code></pre>
<ul>
<li><p><code>HtmlWebpackPlugin</code> – Handles HTML generation and script injection.</p>
</li>
<li><p><code>ModuleFederationPlugin</code> – The core Webpack plugin that lets you expose and consume modules at runtime</p>
</li>
</ul>
<p>Then, define the actual config in <code>module.exports</code>:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// product-app/webpack.config.js</span>

<span class="hljs-keyword">const</span> HtmlWebpackPlugin = <span class="hljs-built_in">require</span>(<span class="hljs-string">'html-webpack-plugin'</span>);
<span class="hljs-keyword">const</span> ModuleFederationPlugin = <span class="hljs-built_in">require</span>(<span class="hljs-string">'webpack'</span>).container.ModuleFederationPlugin;
<span class="hljs-keyword">const</span> path = <span class="hljs-built_in">require</span>(<span class="hljs-string">'path'</span>);

<span class="hljs-built_in">module</span>.exports = {
  <span class="hljs-attr">entry</span>: <span class="hljs-string">'./src/index.js'</span>,                         <span class="hljs-comment">// Entry file to the product app</span>
  <span class="hljs-attr">mode</span>: <span class="hljs-string">'development'</span>,                             <span class="hljs-comment">// Must be production if you go live</span>
  <span class="hljs-attr">devServer</span>: {
    <span class="hljs-attr">port</span>: <span class="hljs-number">3001</span>                                     <span class="hljs-comment">// Product app runs on this port</span>
  },
  <span class="hljs-attr">output</span>: {
    <span class="hljs-attr">publicPath</span>: <span class="hljs-string">'auto'</span>,                            <span class="hljs-comment">// Required for dynamic federation</span>
  },
  <span class="hljs-attr">plugins</span>: [
    <span class="hljs-keyword">new</span> ModuleFederationPlugin({
      <span class="hljs-attr">name</span>: <span class="hljs-string">'productApp'</span>,                         <span class="hljs-comment">// Internal name of the remote app</span>
      <span class="hljs-attr">filename</span>: <span class="hljs-string">'remoteEntry.js'</span>,                 <span class="hljs-comment">// Entry file others will load</span>
      <span class="hljs-attr">exposes</span>: {
        <span class="hljs-string">'./ProductTile'</span>: <span class="hljs-string">'./src/ProductTile.jsx'</span>, <span class="hljs-comment">// Expose this module</span>
      },
      <span class="hljs-attr">shared</span>: {                                   <span class="hljs-comment">// Shared packages if needed</span>
        <span class="hljs-attr">react</span>: { <span class="hljs-attr">singleton</span>: <span class="hljs-literal">true</span> },
        <span class="hljs-string">'react-dom'</span>: { <span class="hljs-attr">singleton</span>: <span class="hljs-literal">true</span> },
      },
    }),
    <span class="hljs-keyword">new</span> HtmlWebpackPlugin({
      <span class="hljs-attr">template</span>: <span class="hljs-string">'./public/index.html'</span>,
    }),
  ],
};
</code></pre>
<p>Now it’s time to use the product app in the main/host app:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// host-app/src/App.jsx</span>

<span class="hljs-keyword">import</span> React, { Suspense } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;

<span class="hljs-comment">// Dynamically import ProductTile from the remote</span>
<span class="hljs-keyword">const</span> RemoteProductTile = React.lazy(<span class="hljs-function">() =&gt;</span> <span class="hljs-keyword">import</span>(<span class="hljs-string">'productApp/ProductTile'</span>));

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">App</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">padding:</span> '<span class="hljs-attr">2rem</span>' }}&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">h1</span>&gt;</span>📦 Host App<span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">Suspense</span> <span class="hljs-attr">fallback</span>=<span class="hljs-string">{</span>&lt;<span class="hljs-attr">div</span>&gt;</span>Loading product tile...<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>}&gt;
        <span class="hljs-tag">&lt;<span class="hljs-name">RemoteProductTile</span> <span class="hljs-attr">title</span>=<span class="hljs-string">"Bluetooth Speaker"</span> /&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">Suspense</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
  );
}
</code></pre>
<p>In React, you can use the <code>React.lazy()</code> function to dynamically import the federated module. It returns a promise that React renders as soon as it’s ready.</p>
<p>That’s it. There’s nothing related to the module federation in the <code>bootstrap.js</code> and <code>index.html</code> files, but regular setup, so you can put whatever you want there:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// host-app/src/bootstrap.js</span>

<span class="hljs-keyword">import</span> React <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;
<span class="hljs-keyword">import</span> { createRoot } <span class="hljs-keyword">from</span> <span class="hljs-string">'react-dom/client'</span>;
<span class="hljs-keyword">import</span> App <span class="hljs-keyword">from</span> <span class="hljs-string">'./App'</span>;

<span class="hljs-keyword">const</span> root = createRoot(<span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">'root'</span>));
root.render(<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">App</span> /&gt;</span></span>);
</code></pre>
<pre><code class="lang-xml"><span class="hljs-comment">&lt;!-- host-app/public/index.html --&gt;</span>

<span class="hljs-meta">&lt;!DOCTYPE <span class="hljs-meta-keyword">html</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">html</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>Host App<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"root"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span>
</code></pre>
<p>And lastly, you can launch the host app:</p>
<pre><code class="lang-bash">npx webpack serve
</code></pre>
<p>That’s it!</p>
<p>Here are a few advantages and limitations of Module Federation, along with popular use cases.</p>
<h3 id="heading-pros-3"><strong>✅ Pros</strong></h3>
<ul>
<li><p><strong>Runtime Integration</strong> – Import remote components after both apps are built</p>
</li>
<li><p><strong>Independent Deployment</strong> – Teams can ship apps on separate pipelines</p>
</li>
<li><p><strong>Code Sharing</strong> – Share common libraries (React, lodash) to reduce duplication</p>
</li>
<li><p><strong>No iframes or wrappers</strong> – Native component integration, not isolated like Web Components</p>
</li>
<li><p><strong>No import maps needed</strong> – Webpack handles all the resolution logic</p>
</li>
<li><p><strong>Works across frameworks –</strong> Can be used in React, Angular, Vue, even Web Components</p>
</li>
</ul>
<h3 id="heading-cons-3"><strong>❌ Cons</strong></h3>
<ul>
<li><p><strong>Tied to Webpack</strong> – <strong>Federation</strong> is Webpack-specific (Vite/Rollup alternatives exist but are not native)</p>
</li>
<li><p><strong>Initial setup is complicated</strong> – Requires per-app Webpack configuration and shared dependency coordination</p>
</li>
<li><p><strong>Runtime failures are possible –</strong> If the remote is down, the host may break unless you handle fallbacks</p>
</li>
<li><p><strong>Version mismatch risks</strong> – Shared libs (like React) must be tightly versioned and aligned</p>
</li>
<li><p><strong>No automatic SSR</strong> – Requires custom hydration logic for federated components</p>
</li>
</ul>
<h3 id="heading-popular-use-cases-3"><strong>👨🏻‍💻 Popular Use Cases</strong></h3>
<p>Use <strong>Module Federation</strong> when:</p>
<ul>
<li><p>You want to build a platform composed of independently deployed apps</p>
</li>
<li><p>You need runtime module loading (not just widgets)</p>
</li>
<li><p>You want to share design systems or UI libraries across apps</p>
</li>
<li><p>Your team is federating complex app sections, not just components</p>
</li>
<li><p>You want to avoid loading dependencies multiple times across apps</p>
</li>
</ul>
<h2 id="heading-other-tools-amp-ecosystem-additions"><strong>Other Tools &amp; Ecosystem Additions</strong></h2>
<p>While iframes, Web Components, single-spa, and Module Federation are the major players in the micro-frontend arena, there’s a growing ecosystem of alternative tools and strategies. They don’t always serve as full micro-frontend methods, but still solve important pieces of the puzzle. Let’s walk through some of the less prominent, yet practical solutions that are worth your attention.</p>
<h3 id="heading-import-maps-native-es-modules"><strong>Import Maps + Native ES Modules</strong></h3>
<p><a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap">Import Maps</a> allow you to define where modules are loaded from, directly in the browser. Combined with native ES module support, they enable zero-build micro frontend setups.</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"importmap"</span>&gt;</span><span class="javascript">
{
  <span class="hljs-string">"imports"</span>: {
    <span class="hljs-string">"ui-library/"</span>: <span class="hljs-string">"https://cdn.example.com/ui/v1.2.3/"</span>,
    <span class="hljs-string">"square"</span>: <span class="hljs-string">"./modules/shapes/square.js"</span>
  }
}
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
</code></pre>
<p>You might’ve noticed that it looks similar to what single-spa + <code>SystemJS</code> does.</p>
<p><strong>Use it when</strong>:</p>
<ul>
<li><p>You want to dynamically load shared libraries (like design systems)</p>
</li>
<li><p>You’re building federated apps without bundlers</p>
</li>
<li><p>You’re targeting modern browsers only</p>
</li>
</ul>
<h3 id="heading-piral-micro-frontends-as-pluggable-portals"><strong>Piral: Micro Frontends as Pluggable Portals</strong></h3>
<p><a target="_blank" href="https://piral.io/">Piral</a> is a specialized framework for building portal-based micro frontends. It provides a structured environment where micro apps (called pilets) can be plugged into a central shell (the Piral instance).</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748797958786/125cdd57-0d2d-4d23-a320-028b081ee989.png" alt="125cdd57-0d2d-4d23-a320-028b081ee989" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<blockquote>
<p><em>Source:</em> <a target="_blank" href="https://piral.io/">https://piral.io/</a></p>
</blockquote>
<p><strong>This framework comes with built-in:</strong></p>
<ul>
<li><p>Routing</p>
</li>
<li><p>Layout orchestration</p>
</li>
<li><p>Shared state</p>
</li>
<li><p>Module loading</p>
</li>
<li><p>Authentication hooks</p>
</li>
</ul>
<p><strong>Great for:</strong></p>
<ul>
<li><p>Enterprise-scale portals</p>
</li>
<li><p>Apps with lots of features teams</p>
</li>
<li><p>Admin dashboards or CMS-heavy UIs</p>
</li>
</ul>
<h3 id="heading-luigi-micro-frontends-sap-style-shells"><strong>Luigi: Micro Frontends + SAP-style Shells</strong></h3>
<p><a target="_blank" href="https://luigi-project.io/">Luigi</a> is a microfrontend framework built by SAP to enable consistent layout shells with side navigation, top bars, permissions, and more.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748798177808/16380085-a4fc-4cc9-9fe2-b44821f9feef.png" alt="16380085-a4fc-4cc9-9fe2-b44821f9feef" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<blockquote>
<p><em>Source:</em> <a target="_blank" href="https://luigi-project.io/">https://luigi-project.io/</a></p>
</blockquote>
<p><strong>This framework comes with built-in:</strong></p>
<ul>
<li><p>Config-driven app registration</p>
</li>
<li><p>Automatic route activation</p>
</li>
<li><p>Role-based access control (RBAC)</p>
</li>
<li><p>Seamless iframe integration with a shell</p>
</li>
</ul>
<p><strong>Great for:</strong></p>
<ul>
<li><p>Intranet tools</p>
</li>
<li><p>Cloud admin panels</p>
</li>
<li><p>Productized dashboards</p>
</li>
</ul>
<h3 id="heading-open-components"><strong>Open Components</strong></h3>
<p><a target="_blank" href="https://github.com/opencomponents/oc">OpenComponents</a> is a framework-agnostic way to build self-contained microservices with UI logic, registered to a central registry.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748798238923/6406ef71-4dde-47bc-8d2b-9476593afdd5.png" alt="6406ef71-4dde-47bc-8d2b-9476593afdd5" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<blockquote>
<p><em>Source:</em> <a target="_blank" href="https://github.com/opencomponents/oc">https://github.com/opencomponents/oc</a></p>
</blockquote>
<p><strong>This framework comes with built-in:</strong></p>
<ul>
<li><p>Server-rendered or client-rendered</p>
</li>
<li><p>REST-like model for UI consumption</p>
</li>
<li><p>Great CDN + registry story</p>
</li>
</ul>
<p><strong>Great for:</strong></p>
<ul>
<li>Used when your company treats UI as deployable microservices, just like APIs.</li>
</ul>
<h3 id="heading-bit-meet-a-composable-architecture">Bit: Meet a composable architecture</h3>
<p><a target="_blank" href="https://bit.dev/">Bit</a> isn’t a micro frontend framework per se, but a component-driven development and distribution platform. It organizes source code into composable components, empowering to build reliable, scalable applications in the era of AI.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748798402542/9fdf7de4-cc1d-41b5-9709-be824c8ffe41.png" alt="9fdf7de4-cc1d-41b5-9709-be824c8ffe41" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<blockquote>
<p><em>Source:</em> <a target="_blank" href="https://bit.dev/">https://bit.dev</a></p>
</blockquote>
<p>Use it alongside Web Components or Module Federation to supercharge reuse. If you want to practice, they have an <a target="_blank" href="https://bit.dev/blog/mastering-micro-frontends-with-module-federation-and-bit-ljn4ruah/">Official Guide</a> on how to master Micro-Frontends with Module Federation.</p>
<p>It’s a great addition when:</p>
<ul>
<li><p>You want to publish reusable components across teams</p>
</li>
<li><p>You need to manage versions, ownership, and discovery</p>
</li>
<li><p>You’re aiming for component-first delivery, not app-first</p>
</li>
</ul>
<h2 id="heading-final-thoughts"><strong>Final Thoughts</strong></h2>
<p>Micro frontends offer immense power, but that power comes with architectural responsibility.</p>
<p>Each method we explored solves a different kind of problem:</p>
<ul>
<li><p>IFrames are secure, but come with complex communication and high isolation.</p>
</li>
<li><p>Web Components are native, framework-agnostic, dependency-free, and perfect for reusable UI Kits</p>
</li>
<li><p>single-spa shines when you need orchestration and multiple SPAs under one shell.</p>
</li>
<li><p>Module Federation is the go-to for runtime code sharing and independent deployment.</p>
</li>
<li><p>And tools like Import Maps, Piral, Luigi, and others fill in the gaps, each in their own way.</p>
</li>
</ul>
<p>There’s no one-size-fits-all solution here, but with the right match for your team structure and product strategy, you can build apps that scale across teams, tech stacks, and time.</p>
<hr>
<p>If you liked this guide, feel free to repost and share it with your friends, colleagues, and social network.</p>
<p>If you want to take your micro-frontend skills to a new level, especially around Web Components, I invite you to check out my best-selling Udemy course called <a target="_blank" href="https://www.udemy.com/course/web-components-api/?couponCode=HERO_START">“Web Components: The Ultimate Guide from Zero to Hero“</a>.</p>
<p>And of course, if you have questions, feedback, or need help with your micro frontend setup, feel free to reach out to me on my social media such as <a target="_blank" href="https://www.linkedin.com/in/andrewmaksimchenko/">LinkedIn</a> / <a target="_blank" href="https://x.com/avmax19">X</a> / <a target="_blank" href="https://t.me/codelikeandrew">Telegram</a>. I’m always happy to chat, connect, and help other devs build amazing things! 💚</p>
<p>Let’s build the IT future we could be proud of! 💪🏼 Thanks for reading — and happy decoupling! 🚀</p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
