Mitchell Hillman

Micro-frontend architecture with iframes

I used an iframe to accomplish a micro-frontend architecture for a legacy application. In my case, the monolithic application required too much refactoring to adopt a framework like single-spa, but it still needed to add a feature which could be independently deployed and shared with other applications (as well as use modern frameworks for its own development). An iframe was a simple solution but required some hacking for the new micro-frontend UI to be seamlessly embedded into its parent. To accomplish this, the embedded micro-frontend used window.postMessage to communicate the UI height and route to the parent.

Parent App (core-ui)

Component to render a micro-frontend in an iframe:

import React, { useEffect, useState } from 'react';

const IframeMFE = ({ 
    defaultHeight
    parentPath,
    src,
    title,
  }) => {

  const [height, setHeight] = useState(defaultHeight);

  const handler = (event) => {
    const { frameHeight, frameURL } = event.data;
    if(frameHeight) {
      setHeight(frameHeight);
    }
    if (frameURL) {
      // used by parent app router to enable deep linking
      window.history.pushState('', '', `${parentPath}/${frameURL}`);
    }
  };

  // intentionally outside of useEffect 
  // to avoid race condition with child app
  window.addEventListener('message', handler);

  return (
    <iframe
      style=
      frameBorder="0"
      scrolling="no"
      title={title}
      src={src}
    />
  );
};

Note: the event listener is intentionally outside of useEffect to avoid a race condition with the child app

Child App (micro-frontend)

Function to test if app is inside an iframe:

const inIframe = () => {
  try {
    return window.self !== window.top;
  } catch (e) {
    return true;
  }
};

React hook to communicate with the parent application using postMessage:

export const usePostFrameHeight = () => {
  useEffect(() => {
    if (inIframe()) {
      const resizeObserver = new ResizeObserver(() => {
        window.parent.postMessage(
          { frameHeight: document.body.clientHeight },
          '*'
        );
      });
      resizeObserver.observe(document.body);
    }
  });
};

Additionally, each time the micro-frontend navigates to a new route it also needs to send a postMessage with the new location:

  window.parent.postMessage({ frameURL: "emails" }, "*");
  navigate(`${constants.rootPath}emails`, { replace: true });

CORS Issues

The child app (micro-frontend) must allow itself to embedded in an iframe. For local development I set the headers for the webpack-dev-server to enable this.

Example webpack.config.js:

module.exports = (env) => ({
  ...
  devServer: {
    ...
    headers: {
      "Access-Control-Allow-Origin": "*",
      "Access-Control-Allow-Methods": 
        "GET, POST, PUT, DELETE, PATCH, OPTIONS",
      "Access-Control-Allow-Headers": 
        "X-Requested-With, content-type, Authorization",
    },
  },
});

Also, The postMessage function takes a second parameter for the targetOrigin. This can be set to * or something more specific. In production this value was set to the deployed URL of the parent application.