DEV Community

Vinodh Kumar
Vinodh Kumar

Posted on • Updated on

React micro-frontend in ember app - Part 1

In this post, we are going to look at how to integrate a React application with its routing mechanisms as a micro-frontend in an Ember application. If you are here then I hope you are already aware of these frameworks so let's look at why we want to integrate React app into an Ember app first.

Why?

The world is moving towards the micro-frontend(MFE) architecture. Ember is best suited for large-scale applications, it has everything you need to create a robust enterprise application, whereas React is a lightweight library with tons of open-source plugins which makes it highly customizable and perfectly fit our MFE need.

Yes, I hear you. There are other frameworks like SolidJs, SvelteJs which are gaining popularity and can be a tough competition to React. These frameworks don't ship any extra framework-related code in your bundle and do most of the heavy lifting at the compile time instead of the browser which makes it extremely lightweight even than React itself. But we all know, React is an undisputed leader in the JavaScript Framework space and the sheer amount of developers who know and already worked on React is mind-boggling. So we picked 'React' to be the framework of choice for the MFE.

Prerequisites

Tools

You will need the following things properly installed on your computer.

PNPM install

  • If you have installed the latest v16.x or greater node version in your system, then enable the PNPM using the below cmd
corepack enable
corepack prepare pnpm@latest --activate
Enter fullscreen mode Exit fullscreen mode
  • If you are using a lower version of a node in your local system then check this page for additional installation methods https://pnpm.io/installation

Ember CLI install

  • Ember CLI is the command line tool provided by Ember to create the Ember app. So let's install it globally.
 npm install -g ember-cli
 ember -v
Enter fullscreen mode Exit fullscreen mode

Setting up the Repo

We are going to set up a mono repo using PNPM workspaces for this POC. If you are trying to add this MFE to an existing app, I highly recommend keeping your apps in a separate Repo. We surely don't want any outdated tools and setup to become a blocker from using the latest technologies for our new MFE.

This is how I set up the mono repo structure using the command line.

mkdir ember-react-mfe
cd ember-react-mfe
pnpm init
git init
echo -e "node_modules" > .gitignore
echo "Monorepo with react MFE in ember app" > README.md
Enter fullscreen mode Exit fullscreen mode

Workspace config

  • Create a pnpm-workspace.yaml file and add the following content
touch pnpm-workspace.yaml
Enter fullscreen mode Exit fullscreen mode
packages:
  - 'apps/*'
Enter fullscreen mode Exit fullscreen mode
  • Create the apps directories in the root.
mkdir apps
Enter fullscreen mode Exit fullscreen mode

Our basic mono repo setup is now done. Let's create our React app first.

React app

  • We are going to use Vite bundler to create our React app.
cd apps
pnpm create vite admin-app --template react-ts
cd ../
pnpm install
npm pkg set scripts.admin="pnpm --filter admin-app"
Enter fullscreen mode Exit fullscreen mode
  • To start the react admin app in the dev server run pnpm admin dev.

  • Let's add the routing capability to our react app by installing the react-router-dom npm package.

pnpm admin add react-router-dom
Enter fullscreen mode Exit fullscreen mode

Sample pages

  • Create the router file with all the necessary routings. For simplicity purposes, I have added all the components in this same file instead of creating separate files.

In an ideal world, it should be lazy-loaded to support code splitting from separate folders and files. Whatever you do in a normal React application can be done without any issues.

touch apps/admin-app/src/router.tsx
Enter fullscreen mode Exit fullscreen mode

Copy the content from router.tsx file from this gist.
https://gist.github.com/vinomanick/919ad40ab98716695837c207526ee156

  • Update the main.tsx file, to support the routing instead of rendering the default App component.
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import { RouterProvider } from "react-router-dom";
import { generateRouter } from "./router.tsx";

ReactDOM.createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
    <RouterProvider router={generateRouter({ basename: "/" })} />
  </React.StrictMode>
);
Enter fullscreen mode Exit fullscreen mode
  • Run the server and make sure all the routes are working as expected.

  • Let's remove the App component-related files and update the index.css as follows.

rm -rf apps/admin-app/src/App.tsx apps/admin-app/src/App.css
Enter fullscreen mode Exit fullscreen mode

Convert the react app to a web component

  • The react app can be directly rendered in the ember app but it is best to wrap the entire application in a web component to have strong encapsulation of styles between the host and the MFE.

  • The main.tsx file is responsible for mounting the react app to the element with id as root on load. We need to change that logic and mount the react app inside an element present in the web component shadow DOM and also load the styles inside the web component as shown below.

import React from 'react'
import ReactDOM from 'react-dom/client'
import indexStyle from './index.css?inline'
import { RouterProvider } from 'react-router-dom'
import { generateRouter } from './router.tsx'

class AdminMFE extends HTMLElement {
  constructor() {
    super()
  }

  connectedCallback() {
    console.debug('Admin MFE element added to app')
    const mountPoint = this.createMountPoint()
    const props: Record<string, any> = this.getProps(this.attributes)
    this.addStyles()
    this.mountReactApp(mountPoint, props.options)
  }

  /**
   *  Creates a mount point and attach it to the shadow dom
   */
  createMountPoint() {
    const mountPoint = document.createElement('div')
    mountPoint.setAttribute('id', 'admin-mfe')
    mountPoint.style.height = '100%'
    mountPoint.style.overflow = 'auto'
    this.attachShadow({ mode: 'open' }).appendChild(mountPoint)
    return mountPoint
  }

  /**
   *  Mounts the react app in to the given element
   */
  mountReactApp(element: HTMLElement, { basename = '' }: { basename: string }) {
    ReactDOM.createRoot(element).render(
      <React.StrictMode>
        <RouterProvider router={generateRouter({ basename })} />
      </React.StrictMode>
    )
  }

  /**
   * Adds the style to the web component as an inline tag
   */
  addStyles() {
    const id = 'admin-style'
    const style = document.createElement('style')
    style.id = id
    style.appendChild(document.createTextNode(indexStyle))
    this.shadowRoot?.appendChild(style)
  }

  getProps(attributes: NamedNodeMap) {
    return [...attributes]
      .filter((attr) => attr.name !== 'style')
      .map((attr) => this.convert(attr.name, attr.value))
      .reduce((props, prop) => ({ ...props, [prop.name]: prop.value }), {})
  }

  convert(attrName: string, attrValue: any) {
    let value: any = attrValue
    if (attrValue === 'true' || attrValue === 'false') {
      value = attrValue === 'true'
    } else if (!isNaN(attrValue) && attrValue !== '') {
      value = +attrValue
    } else if (/^{.*}/.exec(attrValue)) {
      value = JSON.parse(attrValue)
    }
    return { name: attrName, value: value }
  }
}

customElements.define('admin-mfe', AdminMFE)
Enter fullscreen mode Exit fullscreen mode

Local dev server update

  • Update the index.html by removing the root div and adding the web component. The body should be something like this.
<body>
  <script type="module" src="/src/main.tsx"></script>
  <admin-mfe />
</body>
Enter fullscreen mode Exit fullscreen mode
  • Run the dev server and inspect the DOM, you should be able to see the whole app rendered inside and the web component.

Exposing the web component as a loadable script

For Vite bundler, the entry file is the index.html so leaving that all the other compiled assets are automatically fingerprinted. So we won't be able to simply call the bundled index-<hash>.js from the host app. To solve that we need to do the followings,

  • Update the Vite configuration to produce a manifest file with all the fingerprinted assets and its location.
export default defineConfig({
  plugins: [react()],
  build: { manifest: true },
})
Enter fullscreen mode Exit fullscreen mode
  • Create a new entry point to our app in the public folder. Anything added in the public folder will be copied to the dist folder by Vite.
touch apps/admin-app/public/admin-mfe.js
Enter fullscreen mode Exit fullscreen mode
  • Update it with the following content,
export const mountApp = ({ element, options }) => {
  const { baseURL, ...rest } = options;
  if (!baseURL) {
    throw Error(
      'Please provide the baseURL in the options for the admin MFE to load'
    );
  }
  fetch(`${baseURL}manifest.json`)
    .then((res) => res.json())
    .then((data) => {
      import(`${baseURL}${data['index.html'].file}`).then(() => {
        const adminMfe = document.createElement('admin-mfe');
        adminMfe.setAttribute('options', JSON.stringify(rest));
        element.appendChild(adminMfe);
      });
    });
};
Enter fullscreen mode Exit fullscreen mode

This will expose a method called mountApp which can be used by the host app to attach the web component to the host app element.

Building the react app and making it available for the ember app

  • Build the react app in watch mode
pnpm admin build --watch
Enter fullscreen mode Exit fullscreen mode
  • Open a new terminal, run the preview command
pnpm admin preview
Enter fullscreen mode Exit fullscreen mode

The preview command will run a static server at http://localhost:4173 that points to the react app dist folder.

so the host app has to request http://localhost:4173/fw-chat-admin.js which will act as an entry point for our MFE.

Congrats if you made it here, with this we are done with the setup for react application. Let's see how to link and load this app inside the ember app in the next part.

Top comments (0)