Why
We’ve seen some informative articles on how to use React components inside an Angular app, but guides for “using Angular components inside React” were scarce.
The GDC project is in the process of migrating from AngularJS (Angular 1) to React. For quite a while, the React-in-Angular pattern was used to perform a strangler migration one component at a time. As React ate through more and more of the project, we reached a tipping point where it could become the root app, but we were still short a few pieces of somewhat complex Angular directives that prevented us from reaching feature parity.
Being able to reuse those Angular directives as-is allowed us to continue the migration without compromising existing features or our delivery timeline.
Bootstrapping Angular
This part is actually incredibly simple.
Assuming the name of the root module for your legacy Angular app is ngApp
, and the name of the Angular component you want to wrap as a react component is widget
class AngularWidget extends React.Component {
componentDidMount() {
angular.bootstrap(this.container, ['ngApp']);
}
render = () => (
<div
ref={c => { this.container = c; }}
dangerouslySetInnerHTML={{ __html: '<widget></widget>' }}
/>
)
}
Here’s a contrived example using angular-bootstrap-datetimepicker
See the Pen Angular Bootstrap DateTimePicker in React by Chang Wang (@cheapsteak) on CodePen.
This would suffice for completely self-contained components/directives, but what if there are buttons or links in the Angular component that are supposed to take the user to the now react app?
Hijacking ui-router
Assuming ui-router is being used for routing, we only have to intercept calls to $state.go
componentDidMount() {
angular.module('ngApp')
.run(($state) => {
$state.go = (state, params, options) => {
console.log('hijacked ui router');
}
})
angular.bootstrap(this.container, ['ngApp']);
}
You can then choose how the app should handle those requests
Here’s an example that passes it off to react-router v4
See the Pen Angular Hijack ui-router in React by Chang Wang (@cheapsteak) on CodePen.
If the old routes and new routes don’t match 1:1, you could insert a mapping function that maps old routes to new routes. An example of that can be found in our codebase here
Neutralize Angular’s location tampering
A problem we ran into was that after initializing and then navigating away from the Angular component, the browser’s address bar would become “stuck”, always resetting to the route the angular component was on.
Keep an eye on the address bar:
It turns out Angular’s digest cycle checks the browser’s location, and when it notices that the browser’s url is different from what it thinks it should be, it sets the browser’s url to the “correct” value. Since the url is now being handled outside of Angular, we need to disable this behaviour.
Looking at Angular’s source, we see it sets the url via the (🕵️private) $browser
service
- $browser.url is a method that is both a setter and a getter
- when used as a setter, it calls
history.pushState/replaceState
, then returns $browser (possibly for chaining) - when used as a getter, it returns the current location
We need to neutralize the setter without breaking the app. Here’s how:
angular
.module('ngApp')
.run(($browser) => {
$browser.url = function (url) {
if (url) {
// setter is now a noop
return $browser;
} else {
// getter always returns ''
return '';
}
};
})
Note that the above would result in $browser.url()
when used as a getter to always return an empty string. That could be replaced with an actual path if the component needs a specific route or query param (e.g. return 'http://localhost/route/subroute/123?q=4'
instead of return ''
), or even the location
from react-router.
Cleanup
We’re nearly done, just need to clean up the bootstrapped angular app when the React component unmounts.
Here’s a minimal example (you can replace ngApp
with the name of your angular module).
class AngularWidget extends React.Component {
componentDidMount() {
this.$rootScope = angular.injector(['ng', 'ngApp']).get('$rootScope');
angular.bootstrap(this.container, ['ngApp']);
}
componentWillUnmount() {
this.$rootScope.$destroy();
}
render = () => (
<div
ref={c => { this.container = c; }}
dangerouslySetInnerHTML={{ __html: '<widget></widget>' }}
/>
)
}
Summary
It’s always a difficult decision on whether, when, and how to migrate from a framework. For those that have started or are about to begin, I hope this article will help ease and complete that transition.
If you’ve read this far, consider following me on Medium or tweet at me @CheapSteak
You might also be interested in my article on Migrating a legacy frontend build system to Webpack and Quick and Dirty tricks for debugging Javascript, or perhaps Animations with ReactTransitionGroup
Thanks to Ben Hare, Dusan Andric, Jeffrey Burt and Francois Gerthoffert for proofreading and feedback 😄