Modernize Legacy Apps: Integrate React with Existing AngularJS using Single-spa

Manohar HalappaManohar Halappa
5 min read

In modern web development, integrating multiple frameworks and libraries into a single cohesive application can be a daunting task. Developers often face challenges when trying to leverage the strengths of different technologies within a unified user interface. This is particularly evident when attempting to combine legacy systems, such as AngularJS (Angular 1), with more modern frameworks like React (React 18). The complexity of managing dependencies, ensuring seamless interactions, and maintaining a consistent user experience across disparate frameworks can lead to significant development overhead and potential performance issues. This article aims to address these challenges by providing a comprehensive guide on using single-spa, a micro frontend framework, to integrate AngularJS and React into a single UI application. Through detailed steps and configurations, we will demonstrate how to effectively manage and run both frameworks together, streamlining the development process and enhancing the overall application architecture.

Prerequisites

This article assumes you have a basic understanding of npm, node, and JavaScript. You should also have experience with JavaScript-based UI frameworks like Angular, ReactJS, Vue, or Ember. Ensure your webpack, webpack-dev-server, node, npm, and other related dependencies are up to date.

Setting Up single-spa CLI

The single-spa CLI will help in autogenerating managed configurations for webpack, babel, jest, etc. The CLI is recommended when starting a new UI app either in single-spa or any other frameworks as it helps in setting up the basic tooling and configuration for your app. We will be installing the CLI globally.

npm install --global create-single-spa

Step 1: Bootstrap a React Project

First, create a single-spa application for React.

create-single-spa --framework react

The CLI is very helpful and self-explanatory:

  1. Choose "single-spa application/parcel" and hit enter.

  2. Skip other options and add your organisation name when asked.

  3. For configuration options, select the default ones by pressing enter.

This process will also run npm install for you.

Run your ReactJS application:

npm run start

Step 2: Bootstrap an Angular Project

Now we will create a new Angular application. Use the Angular CLI (ng) with routing and a prefix. The prefix is important if we have multiple Angular apps to ensure component selectors don't have the same name.

ng new my-angular-app --routing --prefix my-angular-app
cd my-angular-app

Adding single-spa Configuration to the Angular Project

For AngularJS (Angular 1), refer to single-spa documentation.

ng add single-spa-angular

Edit app-routing.module.ts to add a provider for APP_BASE_HREF.

providers: [{ provide: APP_BASE_HREF, useValue: '/' }]

Ensure you have a route specified for EmptyRouteComponent:

{ path: '**', component: EmptyRouteComponent }

Add the following build script inside your package.json:

"serve:single-spa:my-angular-app": "ng s --project my-angular-app --disable-host-check --port 4200 --deploy-url http://localhost:4200/ --live-reload false"

Run the Angular app:

npm run serve:single-spa:my-angular-app

Step 3: Creating a Root Config

The root config exists only to start up the single-spa applications. It consists of:

  • An HTML file that is shared by all single-spa applications.

  • A JS file which calls singleSpa.registerApplication().

We will use import-maps to import React and Angular components in our single-spa root config HTML.

npx create-single-spa root-config

After running the command, choose the last option to create a root-config. Skip the other configurations by pressing enter.

Configuring the Root Config

Navigate to index.ejs inside src and make the following changes:

  1. Search for <% if (isLocal) { %> <script type="systemjs-importmap">.

  2. Add the imports from your Angular and ReactJS apps. Here is an example of how it should look:

<% if (isLocal) { %>
<script type="systemjs-importmap">
{
    "imports": {
    "single-spa": "https://cdn.jsdelivr.net/npm/single-spa@5.9.1/lib/system/single-spa.min.js",
    "@app/legacyAngularApp": "/main.js",
    "react": "https://cdn.jsdelivr.net/npm/react@18.2.0/umd/react.production.min.js",
    "react-dom": "https://cdn.jsdelivr.net/npm/react-dom@18.2.0/umd/react-dom.production.min.js",
    "@react-app/react-app": "//localhost:8081/react-app-react-app.js"
       }
    }
}
</script>
<% } %>

Ensure that react, react-dom, and single-spa are also present inside the imports JSON. There should be a total of six imports in the JSON. Double-check the port numbers for root-config, single-spa-react, and my-angular-app.

  1. Remove the comment against the zone.js import:
<script src="https://cdn.jsdelivr.net/npm/zone.js@0.10.3/dist/zone.min.js"></script>
  1. Add the following function in the JS file inside the src:
function loadWithoutAmd(name) {
  return Promise.resolve().then(() => {
    let globalDefine = window.define;
    delete window.define;
    return System.import(name).then((module) => {
      window.define = globalDefine;
      return module;
    });
  });
}
  1. Add the registration code for ReactJS and Angular. The registerApplication function takes name, system import name, and other parameters. Ensure the names match with those declared previously in index.ejs and package.json of the respective apps.
import { registerApplication, start } from "single-spa";

registerApplication(
  "@apps/single-spa-react",
  () => System.import("@abhi/single-spa-react"),
  true
);

registerApplication(
  "@apps/my-angular-app",
  () => System.import("@abhi/my-angular-app"),
  true
);

start({
  urlRerouteOnly: true,
});

Finally, all configurations are ready. Execute npm run start inside the root-config folder.

Open your browser and visit http://localhost:9000. Your Angular and React content should be rendered. Note that the order of rendering may vary based on how SystemJS imports the JavaScript files using import-maps and which file loads first in the index.ejs inside the root-config.

Additional Notes:

1) If Angular 1 is your base application you have have to update your main.js to have the below code

System.register([], function (_export) {
  return {
    execute: function () {
      _export(
        window.singleSpaAngularjs.default({
          angular: angular,
          mainAngularModule: "main-module",
          uiRouter: true,
          preserveGlobal: false,
        })
      );
    },
  };
});

2) And in index.html we will have to add react cdn links and import the spa applications

<!DOCTYPE html>
<html>
  <head>
    <script src="https://cdn.jsdelivr.net/npm/single-spa-angularjs@4.3.1/lib/single-spa-angularjs.min.js"></script>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.9/angular.min.js"></script>
    <script src="//unpkg.com/@uirouter/angularjs/release/angular-ui-router.min.js"></script>
    <script type="systemjs-importmap">
      {
        "imports": {
          "single-spa": "https://cdn.jsdelivr.net/npm/single-spa@5.9.1/lib/system/single-spa.min.js",
          "@app/legacyAngularApp": "/main.js",
          "react": "https://cdn.jsdelivr.net/npm/react@18.2.0/umd/react.production.min.js",
          "react-dom": "https://cdn.jsdelivr.net/npm/react-dom@18.2.0/umd/react-dom.production.min.js",
          "@react-app/react-app": "//localhost:8081/react-app-react-app.js"
        }
      }
    </script>
    <script src="https://cdn.jsdelivr.net/npm/systemjs@6.8.3/dist/system.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/zone.js@0.11.3/dist/zone.min.js"></script>
    <script>
      System.import("@app/legacyAngularApp");
      System.import("@react-app/react-app");
    </script>
  </head>

  <body>
    <p>Click on the links to navigate to Angular micro app</p>
    <br />

    <p>
      <a href="#!/home">AngularJs App</a>
      <br />
      <a href="#!/react">React App</a>
    </p>
    <ui-view></ui-view>
    <script>
      System.import("single-spa").then(function (singleSpa) {
        console.log("Mano test message", singleSpa);
        window.singleSpa = singleSpa;
        window.singleSpa.registerApplication({
          name: "legacyAngularApp",
          app: () => System.import("@app/legacyAngularApp"),
          activeWhen: ["#!/home"],
        });
        window.singleSpa.registerApplication({
          name: "newReactApplication",
          app: () => System.import("@react-app/react-app"),
          activeWhen: ["#!/react"],
        });
        window.singleSpa.start();
      });
    </script>
  </body>
</html>

3) If using single spa for modern react application then ensure you upgrade you react versions to latest as it might give errors for incompatible versions

References:

https://single-spa.js.org/docs/examples/

https://single-spa.js.org/docs/create-single-spa/

https://github.com/single-spa/single-spa-angularjs

https://github.com/single-spa/single-spa-react

0
Subscribe to my newsletter

Read articles from Manohar Halappa directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Manohar Halappa
Manohar Halappa