Micro Frontends - how I built an SPA with Angular and React?

in JavaScript, Micro Frontends

If you're working in a big company, you're probably struggling to work on a single application with multiple teams. You have the large codebase, bunch of components, pages, everything is connected and you're always overlapping your work with some other team. Yeah, I know, that sucks, I've been there and tried a bunch of things to fix that issue.

When you work on a single app with multiple teams, on different functionalities, you need to have:

  1. A big shared codebase that everybody maintains and has routing, session and authentication functionalities
  2. Multiple modules separated into repositories that each team maintains
  3. The bundling and deployment system that bundles everything into one big monolith app and puts that on a server

I have been in companies that have implemented this strategy and this is fine, or I can say it's perfect, but not for all use cases. When it becomes bigger and you want to scale, you're stuck.

The solution to this problem is very simple. Welcome to the era of the Micro Frontends.

What are Micro Frontends? It is a new approach to building frontend applications. We're deprecating the good old monolith methodology and starting to write the apps in multiple frameworks (and even vanilla JS), loading them together using the same router, same domain and without refreshing the page. This gives us the ability to work separately and make separated single page applications that can work independently, isolated and tested. We can load and deploy them and use them together under one building system.

I am the big fan of the single-spa module. Using single-spa we can make the main parent app that has routing and authentication (user sessions etc.) implemented and a bunch of child apps that work like the independent apps. They're loaded using lazy loading without page refresh and can be loaded on the same or different pages.

Main features:

  • Use multiple frameworks on the same page without refreshing the page (React, AngularJS, Angular, Ember, or whatever you're using)
  • Write code using a new framework, without rewriting your existing app
  • Lazy load code for improved initial load time
  • Hot reload entire chunks of your application (instead of individual files).

TL;DR

I appreciate everyone's time, if you want to see the code without any explanation, here you go, everything is in the repo: https://github.com/IvanJov/react-angular-single-spa

How to do it?

So far you've read what is single-spa and why is it so awesome. It's time to build a real app and show how can we integrate two main frameworks, React and Angular to work together. We'll finally unite Angular and React fans. Isn't that awesome? 😅

We're going to make a simple index.html file, inject React and Angular and exchange messages between them. Should be really awesome! Let's start.

App structure

Let's see how are we going to structure our files. This is how it should look like at the end:

First thing we need to do is to initialize NPM project:

npm init

You can take .babelrc, package.json, tsconfig.json, webpack.config.js and other boilerplate from the Github repo. You'll also need to install a lot of dependencies to make this work, with:

npm install

We'll now go to the Angular and React part, and their communication and initialization on the page.

Root application

First, let's make root-application.js file. It should have everything for rendering our child apps. It should be created in root-application directory:

import { start, registerApplication } from 'single-spa'

const hashPrefix = prefix => location => location.hash.startsWith(`#${prefix}`)

registerApplication('react', () => import('../react/index.js'), hashPrefix('/'))
registerApplication('angular', () => import('../angular/index.js'), hashPrefix('/'))

start()

In this file, we're importing index files from react and angular directories and we initialize them on the page when the router hash starts with the /. In this example we want them to be on the same page, to show how they can communicate together. hashPrefix is a good method and you can use it to mount child app on any other page.

Communication

The communication between React and Angular apps can be tricky. I recommend Eev event bus. It's small, fast and zero-dependency event emitter that will help us to exchange information between React and Angular app. Let's see how that should look:

import Eev from 'eev'

export const e = new Eev()

export default e

This is the index.js file in the event-bus directory. It just initializes Eev and exports it. We'll import it in both child apps and use it to exchange data.

React

The first thing we'll work on is the React app. We'll start with the root-component.js. It's a React component that's rendering some text, emitting and listening for the data and renders it.

import React from 'react'
import e from '../event-bus'

export default class Root extends React.Component {
  constructor(props) {
    super(props)

    this.state = {
      message: 'When Angular receives message, we should see a confirmation here 😎'
    }

    this.messageHandler = this.messageHandler.bind(this)
  }

  componentDidMount() {
    e.on('received', this.messageHandler)
  }

  componentDidUnmount() {
    e.off('received', this.messageHandler)
  }

  messageHandler(message) {
    this.setState({
      message: message.text
    })
  }

  sendMessage() {
    e.emit('message', { text: 'Hello from React 👋' })
  }

  render() {
    return (
      <div style={{marginTop: '10px'}}>
        <h1>This was written in React</h1>

        <p>
          <button onClick={this.sendMessage}>
            Send a message to Angular
          </button>
        </p>

        <p>
          {this.state.message}
        </p>
      </div>
    )
  }
}

When it comes to the React app, we just have 2 files, index.js, and root-component.js file. In the root component, at the top, we just import React and Eev instance. Next, we make a constructor and a component state object. The state has a message key and a placeholder value that will be replaced when we get a confirmation message from the Angular app. Next, we use componentDidMount lifecycle to listen for the event received that's going to be emitted from the Angular app when it receives a message from the React. And finally, inside the render method, we render an H1 element, a paragraph with a button and a paragraph with a message variable from the state. Button has the onClick event, it will emit the message to the Angular component. There's also a nice usage of the componentDidUnmount that removes the event handler when the React app is unmounted and saves some memory.

We'll need a index.js file, the export of the React child app:

import React from 'react'
import ReactDOM from 'react-dom'
import singleSpaReact from 'single-spa-react'
import Root from './root.component.js'

const domElementGetter = () => {
  let el = document.getElementById('react')
  if (!el) {
    el = document.createElement('div')
    el.id = 'react'
    document.body.appendChild(el)
  }

  return el
}

const reactLifecycles = singleSpaReact({
  React,
  ReactDOM,
  rootComponent: Root,
  domElementGetter,
})

export const bootstrap = props => reactLifecycles.bootstrap(props)

export const mount = props => reactLifecycles.mount(props)

export const unmount = props => reactLifecycles.unmount(props)

This is the code that will tell single-spa how to bootstrap, mount and unmount the React app. We're using the single-spa-react module to help us export those three methods.

Angular

Now it's time for the next part, Angular app. Angular is a bit more complicated than React, so we need couple more files for this. The first one we'll create is index.component.ts, the file for the code of the component:

import { Component, ChangeDetectorRef, Inject } from '@angular/core'
import e from '../event-bus'

@Component({
  selector: 'AngularApp',
  template: `
	<div style="margin-top: 100px;">
      <h1>This was written in Angular</h1>
      <p>{{message}}</p>
	</div>
  `,
})
export default class AngularApp {
  message: string = "Message from React should appear here 😱"

  constructor(@Inject(ChangeDetectorRef) private changeDetector: ChangeDetectorRef) {}

  ngAfterContentInit() {
    e.on('message', message => {
      this.message = message.text
      this.changeDetector.detectChanges()
      this.returnMessageToReactWhenReceived()
    })
  }

  returnMessageToReactWhenReceived() {
    e.emit('received', { text: 'Woohoo! Hello from Angular! 🎉' })
  }
}

In this file, we're creating an Angular component that just renders a div with the message that will come from the React app. When the message is received, Angular is sending a response to the React app, that will be rendered on the React's side.

Inside the constructor, we inject the ChangeDectorRef. Since the change of the message will come from the Eev event, Angular won't re-render automatically and we'll need to tell it when to render. Then, we're using a method from the Angular Component Lifecycle, ngAfterContentInit. It is called when all content has been rendered for the first time. That's the place where we want to listen for the Eev event. Inside it, we call an Eev instance on method and listen for a message event that will come from our React app. There we change the message, tell Angular to re-render using this.changeDetector.detechChanges() function and call custom returnMessageToReactWhenReceived function that will send a message back to the React and say that Angular has successfully got the message.

The next file we need is main-module.ts, the main Angular module file.

import { NgModule } from '@angular/core'
import { BrowserModule } from '@angular/platform-browser'
import AngularApp from './index.component.ts'
import { enableProdMode } from '@angular/core'
import { APP_BASE_HREF } from "@angular/common"

enableProdMode()

@NgModule({
  imports: [
    BrowserModule,
  ],
  providers: [{ provide: APP_BASE_HREF, useValue: '/angular/' }],
  declarations: [
    AngularApp
  ],
  bootstrap: [AngularApp]
})
export default class MainModule {
}

And the last file we need is index.js to export the child component to the single-spa root component:

import 'zone.js'
import 'reflect-metadata'
import singleSpaAngular from 'single-spa-angular2'
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'
import mainModule from './main-module.ts'
import { Router } from '@angular/router'

const domElementGetter = () => {
  let el = document.getElementById('angular')
  if (!el) {
    el = document.createElement('div')
    el.id = 'angular'
    document.body.appendChild(el)
  }

  return el
}

const ngLifecycles = singleSpaAngular({
  domElementGetter,
  mainModule,
  angularPlatform: platformBrowserDynamic(),
  template: `<AngularApp />`,
  Router,
})

export const bootstrap = props => ngLifecycles.bootstrap(props)

export const mount = props => ngLifecycles.mount(props)

export const unmount = props => ngLifecycles.unmount(props)

Same as React, this code will tell single-spa how to bootstrap, mount and unmount the Angular app. We're using the single-spa-angular2 module to help us export those three methods.

Now we have exported the Angular app and completed our first Micro Frontend application. The demo can begin!

Demo!

As you probably saw in the package.json, for running the app we just need to execute:

npm start

Then just open this link in the browser

http://localhost:9090/#/

Notice the hash, we are loading React and Angular app on the / of the hash.

After opening the page you should see something like this:

Looks cool, right? You have just rendered your first app that has been made from React and Angular! To see them working together, click on the Send a message to Angular button. You should see this:

As you see, the Angular app has rendered Hello from React 👋 text and sent a message to a React app. React app has rendered the Woohoo! Hello from Angular 🎉 message. We have successfully exchanged async messages between two apps on the same page and they have re-rendered and showed us the result. Isn't that awesome? This small example tells us that we can really have multiple apps on the same page, made using different frameworks, from different teams, that work together.

Conclusion

This is the simplest example of the Micro Frontends. It's a new topic that's still developing and we can take part in it. When you first look at it, it's fun, but it's also really powerful and useful in big organizations. You can combine smaller apps and create big frontend applications. We can finally scale on the frontend. I hope that you learned something new, or just had some fun while you were reading this 😄

Comments