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
- 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
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
Workspace config
- Create a
pnpm-workspace.yaml
file and add the following content
touch pnpm-workspace.yaml
packages:
- 'apps/*'
- Create the
apps
directories in the root.
mkdir apps
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"
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
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
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>
);
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
- Copy the content of index.css from
index.css
file from this gist. https://gist.github.com/vinomanick/919ad40ab98716695837c207526ee156
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 asroot
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)
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>
- 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 },
})
- 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
- 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);
});
});
};
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
- Open a new terminal, run the preview command
pnpm admin preview
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)