Migrating from AngularJS 1.x to React/Vue with the Strangler Pattern

Alwyn Tan
Data.gov.sg Blog
Published in
7 min readOct 9, 2019

Text: Alwyn Tan, Senior Software Engineer

[This post was written to prepare for talk.js October 2019 in Singapore]

Introduction

Migrating from a legacy framework has its risks, roughly proportional to the size of the codebase to change. That said, the risks involved in migrating from AngularJS to a more modern JavaScript framework can be mitigated by moving code incrementally, using the Strangler Pattern. This is possible since these frameworks fundamentally do the same thing: programatically render a web page.

This blog post hopes to cover and point to material describing the techniques to perform an incremental migration away from AngularJS with the Strangler Pattern.

Motivations

JavaScript frameworks come and go. However, few such developments have had such a profound impact as the announcement of Angular 2 in October 2014. Along with a slight tweak in name per the Branding Guidelines, the new major version introduced breaking changes, at a time when other frameworks like React and Vue were also growing in popularity. Developers were either forced to undertake major changes to their codebase to move to React, Vue or Angular, or hang onto AngularJS for as long as the developer community was willing to support its ecosystem.

The developers for FormSG know this too well. A fork of TellForm, FormSG inherited the use of AngularJS in its frontend. Its adoption by many government agencies and aggressive feature roadmap meant that a frontend rewrite in a more modern framework had to be deferred, while the codebase grew. FormSG’s stability and availability became increasingly important, even as the Angular community announced that long-term support for AngularJS would cease in 2021.

Any attempt to replace the framework would thus have to prevent or minimise the likelihood of service disruption. This meant that replacing everything in one go was out of the question, as there would be too many things to look out for when releasing the change. An incremental approach would not completely eliminate the risk of a migration, but would at least mitigate it by allowing the roll-out (or roll-back) to be gradual. This post came about largely out of the research done into this approach.

Techniques

A typical frontend application would not only have components (controllers, views and directives in AngularJS), but scaffolding as well (which includes URL routing frameworks like angular-ui-router, and things for state management like services and factories). Migration would involve rewriting either the components first, or the scaffolding first, as we would cover later.

In both strategies, one framework will bootstrap the other within a provided DOM element acting as a container, with data passing between the two. This is best illustrated with the following:

Mounting React/Vue onto AngularJS

When rewriting components, AngularJS has to bootstrap React or Vue to render its items within the container element provided by AngularJS.

Mounting a simple React component onto AngularJS, adapted from this post by Sebastian Fröstl. Click on HTML and Babel buttons to reveal source

Note the following from above:

  1. we hook into $onChanges in the AngularJS component adapter to render the React component using ReactDOM;
  2. data injected into the adapter can be further injected into the React component as properties, and;
  3. since React does not clean up components that are unmounted automatically, we use $onDestroy() to signal React to do so.

Similar results can be had for Vue:

Mounting a simple Vue component onto AngularJS. Again, click on HTML and Babel buttons to reveal source

Mounting AngularJS onto React/Vue

Basic Premise

Having an AngularJS component rendered within a React or Vue application involves the direct embedding of HTML within a component, through the dangerouslySetInnerHtml element attribute in React, or the equivalent v-html in Vue. AngularJS needs the reference to the element it will bootstrap itself to, so a ref=”<some name>” attribute has to be added to the element, accessed by React and Vue as this.<some name> and this.$refs.<some name> respectively. We also need to hold the $rootScope somewhere in the mounting component so that we can call $rootScope.destroy() to clean up AngularJS when the component is about to destroyed.

Mounting an AngularJS date picker onto React. CodePen adapted from the original by Chang Wang, found here
Mounting an AngularJS date picker onto Vue. Ported to Vue from the above

Routing

Migrations involving angular-ui-router tend to be more involved. Once AngularJS has bootstrapped onto React/Vue, we have to manipulate the $state object provided by angular-ui-router, changing $state.go to redirect to react-router, vue-router or @uirouter equivalents. $browser.url() also has to be overridden when setting up AngularJS through angular.module(), to prevent it from interfering with manipulation of browser history.

Redirecting angular-ui-router to react-router. CodePen adapted from the original by Chang Wang, found here
Redirect angular-ui-router to vue-router. Ported to Vue from the above

State Management

AngularJS has historically relied on the ad-hoc use of services and $rootScope to manage state. In more recent times (and frameworks), a more systematic approach involving a state management library like Redux or Vuex has gained favour. AngularJS services should hence be rewritten to redirect to such stores:

Using Redux to back a service that serves a date to the datetimepicker widget
Using Vuex to back a service that serves a date to the datetimepicker widget

The Strangler Pattern

The general idea for a migration is to introduce parts rewritten using the chosen new framework, and shuttle data and events back and forth from the existing application to those parts. Over time, the newly written parts of the application would form the bulk of the codebase as more functionality is ported away from AngularJS.

Martin Fowler likens this to strangler fig trees, which grow on a host, eventually strangling the host tree once it successfully takes root and outcompetes the host for resources. While this pattern has been more closely associated with migrating legacy backend systems, it can also be applied to some frontend framework migrations, particularly those which involve mounting components onto the DOM.

There are two strategies to undertake a strangler migration of an AngularJS application using the techniques described earlier —

Components first, scaffolding later: the lower risk of the two, this defers the change of the framework to the end by starting with the components, which tend to have a smaller impact across the application, taking on the relatively more complex scaffolding later. That said, since replacing AngularJS for good takes place only close towards the end of the migration, if this cannot be done for one reason or another, significant dev time invested into the effort would be at risk.

Scaffolding first, components later: riskier, but may give greater assurance that the migration can be done, by tackling the scaffolding first and dealing with the biggest part of the risk at the beginning of the migration. Once that is achieved, the rewriting of components is comparatively systematic. By rewriting the scaffolding first, we also have a firmer idea of how the new application and its components would handle non-trivial things like propagating events and state management.

Strangler Pattern Migrations for the Frontend

A third strategy could be achieved by combining the two: write a few small components first, then get to a point where it makes sense to cut-over the scaffolding to the new framework, then spend the rest of the time dealing with the harder-to-replace components.

Gotchas

Making two frameworks interoperate when they assume they have control over the frontend will no doubt be fraught with problems. These are just some of them:

Migrating to React

  • Binding data to components in AngularJS (and Vue) is two-way, that is, changes that happen within the component will be reflected in the data that is bound to it. React uses one-way binding by design, so rewritten components will have to use hooks, callbacks or similar mechanisms to propagate changes out of the component
  • react-router drives changes and transitions purely through URLs, and delivers state parameters solely through those. This contrasts with angular-ui-router (and its framework-agnostic descendent @uirouter), as well as vue-router, which allow for more flexible routing functionality. You may wish to consider using @uirouter/react instead to ensure a smoother migration. More information about the differences between react-router and @uirouter/react can be found at Marco Botto’s blog post.

Migrations in General

  • Because incremental migrations involve having two frameworks running together at some point, be prepared for an increased memory footprint as well as a larger bundle size. Also prepare to update the build tooling (ie, Webpack and Babel) to support the new framework.
  • Each AngularJS component will be mounted onto React/Vue as its own application in isolation. As such, AngularJS components mounted at separate points in a React application will not receive events from each other, and will have to do so via a separate EventEmitter, as illustrated here.
  • ngAnimate will generally not work for reasons this author has yet to realise, and will have to be replaced early in the migration

Acknowledgements

This post (and the talk that came out of it) has been made using material adapted in gratitude from the following sites, without which this could not happen:

Our journey migrating 100k lines of code from AngularJS to React (Chapter 1) - Sebastian Fröstl, Small Improvements Engineering Blog

Using AngularJS components & directives in React - Chang Wang aka @CheapSteak, OICR SoftEng Blog, Ontario Institute for Cancer Research

Further Resources

Migrating one of the first AngularJS example applications to React (first) or Vue (second). The commit histories for both (as linked) are more informative than the final state of the codebase.

Introducing React/Vue to a more complex AngularJS application, replacing angular-ui-router with @uirouter/react or vue-router.

--

--