Modernize Legacy Apps: Integrate React with Existing AngularJS using Single-spa
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:
Choose "single-spa application/parcel" and hit enter.
Skip other options and add your organisation name when asked.
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:
Search for
<% if (isLocal) { %> <script type="systemjs-importmap">
.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
.
- 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>
- 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;
});
});
}
- 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 inindex.ejs
andpackage.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/
Subscribe to my newsletter
Read articles from Manohar Halappa directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by