gotchas”, and weren’t going to end up with a bunch of components that don’t fit together with React running the show instead of Angular.
Next is deciding how we’re going to run these pages at the same time. We investigated as much as we could to help with this problem. The app would be served statically from a file system — no server-side rendering. The two viable options we found were loading React or Angular bundles depending on the route, and the single-spa library.
The first idea worked like this: React pages would have their own index.html
files inside the paths that were relevant to them. This HTML would contain a script tag that would load the React JS bundle. The Angular bundle would then be served up on all other routes like a normal SPA with native history (no hash-prefixed paths in the URL).
For example:
index.html
angular.bundle.js
react.bundle.js
orders/
└── index.html
If you visit example.com/customers
then fall back to loading /index.html
with the Angular script tag. If you visit example.com/orders
it will find /orders/index.html
which will have the React script tag.
The main problems with this approach included requiring separate index.html files in every path that used React. We were letting the file system decide which paths did what, therefore limiting which parts of the app we could make React. It also created difficult problems with client-side routing. We found it tricky getting the correct app to load when the user navigates around.
I wasn’t too happy with single-spa either. It was a bit too complicated for our use case. We were only interested in moving one app over to another, not the vaguely-defined trend of micro-front-ends. It seemed to run both frameworks side-by-side in the same page, which has the smell of unforeseen consequences.
I also wasn’t keen on relying heavily on a large codebase we haven’t written for something like this. Something as important as bootstrapping the app should be reasonably simple and well understood by the team, in case it went wrong in production.
The solution we found took ideas from both. Instead of having an index.html
for each route, we have one, containing a “bootstrap” JS file that picks the React/Angular JS bundle client-side, and appends it to the document.
Here is a basic example of that:
(function() {
const useReact = window.location.pathname.indexOf('/react') === 0;
const scriptSrc = useReact ? 'react.bundle.js' : 'angular.bundle.js';
const script = document.createElement('script');
script.setAttribute('src', scriptSrc);
document.body.appendChild(script);
})();
It’s surprisingly simple. We can set the useReact
boolean how we want, although we still have it based on the route (“/react” is just an example here). It also works for selecting the right stylesheet, you can append the <link>
for that here too.
To get around client-side routing, we created Link
components in each framework that would reload the page when used. The browser is able to detect whether it was a full page load or a history.push()
so back and forward works too.
In testing, we found no real-world performance issues loading the page. The only thing a user might notice is the full page load between frameworks. As this is usual for a non-SPA (Single Page App) website, it shouldn’t be an issue. Hopefully this approach continues to work out going forward!🤞
We decided to make this project a monorepo, rather than split Angular and React into separate git repos. It becomes considerably easier to create code changes that touch both sides of the app this way.
We looked at tools that offer ways of managing multiple JS projects in one repo. Lerna does this with features to run multiple dependency installs and JS scripts, but it is mainly suited for managing multiple versions of publishable libraries.
Yarn offers workspaces, a way of splitting your dependencies, making them easier to manage. But we didn’t want to move from npm for package management. We tried in the past and the setup didn’t work for our project. Maybe npm workspaces in v7 is a good move in the future.
We did, however, like the idea of “packages” (aka… folders) as a way of separating the code between frameworks. So this is the format we went for:
tsconfig.json
package.json
packages/
├── angular/
│ ├── scripts/
│ ├── styles/
├── react/
│ ├── scripts/
│ ├── styles/
├── shared/
│ ├── scripts/
│ ├── styles/
└── assets/
└── images/
(etc.)
A global package.json and TypeScript config, and separate folders for framework specific code. The ‘shared’ folder contains code that is used by pages in both frameworks. We try to share and reuse as much as possible. This is made easier using TypeScript path mapping.
To generate Angular and React JS bundles, we extended our Webpack config like this:
entry: {
angular: 'packages/angular/scripts/app.module.ts',
react: 'packages/react/scripts/app.tsx',
},
output: {
path: 'dist',
filename: '[name].bundle.js',
}
That produces two files, angular.bundle.js
and react.bundle.js
. Nice!
Here are some more tips we’ve tried to work by:
I hope some of the approaches I’ve written about here are of help if you ever need to migrate web frameworks too.