article cover

Migrations

Migrating a large scale arch from AngularJS to React

~ 13 minute read

Adrian Apostol
Senior Software Engineer

06 Dec 2021

All large-scale web applications start with one of the most important technical discussions: architecture and frameworks. For the frontend part, this discussion is usually a standoff between two of the most used frameworks: Angular or React. During this initial phase of the project, the team would review the benefits of using one or the other, address any concerns that may surface and then make a decision that will basically draw the direction in which the project will go.

As time goes by and the project starts to change based on feedback, analysis, testing etc. in some cases the team might say to themselves: we should’ve gone the other way. Even with thorough preparations and analysis of the features that the application initially starts with, in several years the changes could be so deep that the framework chosen is no longer the viable option. This usually leads to one of the largest refactorization that a project may go through: framework migration. In our case it is a switch from AngularJS to React.

As with any large-scale refactor we started with several considerations that we wanted to follow:

  • Don’t impact performance – even with the current framework and code we struggled with performance on parts of the application, mostly due to the type of operations that we do on the FE part (large data filtering, manipulation, UI redrawing of charts etc), so we all agreed performance should not be hit by the switch any more
  • Don’t impact application development – as this is already a live application and the customers heavily rely on it, we cannot allow ourselves to impact the delivery of new features. So, we needed to do the switch while delivering new features and fixing any new bugs
  • Don’t duplicate work – since the team is large, we wanted to keep everyone in the loop when any new component that may be used across the entire app. By doing this we would limit the time spent on issues / bugs on components that are currently being switched to react. Also, any smaller component that could be used application-wide would be presented to the team, so it can be included where possible

To have a proper direction and way to go, we have initially set a destination – an architecture of the application in react. We have considered all current application needs, future development that we thought of and the above conditions and created the scaffold of where we want to be once the conversion is over.

Once this was set, we have decided on a Depth First approach. We started changing the smaller components first, going up the hierarchy as the components got converted. This would allow us to make smaller steps and closely monitor the above conditions. We have decided that any new feature would be developed directly in React. If there was a component that needed improvements that was in AngularJS then we would convert that first, then create the new functionality. Any bugs that were on AngularJS components would be analyzed to see if it’s a good opportunity for conversion.

Each time we changed a component from AngularJS to React we would follow several tips:

  • If it’s a general component (dropdown, modals etc.) then spread the word so the entire application can use this new version instead of the old AngularJS one. By also creating a common library of components it made it easier to follow the same UI patterns and have a common look & feel of the application
  • Once the conversion is over, do performance tests on the before / after versions. We would monitor this closely since our goal was to improve performance when possible, and by no means decrease it. Make sure that fast switching props will not trigger multiple unnecessary re-renders of the components
  • If it’s a component that also has children, convert that first and try to make it more abstract / general so it can be reused
  • Any dependency injections, store listeners would be done via a Higher Order Component so we can easily change the source for these once we reach final stages of conversion

Next, we will present a couple of cases with conversions, showing some of the steps we did.

This is a fairly simple example:

1// journalMetadata.component.html
2<div class="flexColumnContainer">
3    <span class="specCreatedBy">{{$ctrl.name}}</span>
4    <span class="specUpdatedAt">{{$ctrl.date}}</span>
5</div>
6
7// journalMetadata.component.ts
8imports ...;
9import template from './journalMetadata.component.html';
10
11angular.module('Sq.Annotation').component('sqJournalMetadata', {
12    template,
13    bindings: {
14        name: '<',
15        date: '<'
16    }
17});
18
19// journalMetadata.tsx
20export const JournalMetadata = (props) => {
21    const { name, date } = props;
22
23    return (
24        <div className="flexColumnContainer">
25            <span className="specCreatedBy">{name}</span>
26            <span className="specUpdatedAt">{date}</span>
27        </div>
28    );
29};
30
31export angularJournalMetadata = react2angular(JournalMetadata, ['name', 'date']);
32    

Since the component is simple and doesn’t have any controller, most of the react component is created by copying the html code as the return value. The props that will be received are sent as a list to the react2angular function, so it knows what we’re expecting. The bindings will be done automatically and the react component will receive the props as expected.

Let’s see a more complex example.

1// journalEditor.component.html
2<div id="journalEditorToolbarContainer"></div>
3<div class="flexRowContainer"
4sq-resize-notify="$ctrl.resize(height, width)"
5sq-resize-notify-initial="true">
6    <textarea id="journalEditor"
7    document="$ctrl.document"
8    is-editing="$ctrl.isEditing">
9    </textarea>
10    <div class="fr-view"
11    id="specJournalEntry"
12    ng-if="!$ctrl.isEditing"
13    ng-bind-html="$ctrl.trustedDocument"></div>
14</div>
15
16// journalEditor.component.ts
17angular.module('Sq.Annotation').component('sqJournalEditor', {
18    controller: 'JournalEditorCtrl',
19    template,
20    bindings: {
21        id: '<',
22        document: '<',
23        isEditing: '<',
24        placeholderText: '<',
25        documentChanged: '&',
26        beforeOnInit: '&',
27        afterOnInit: '&',
28        onDestroy: '&'
29    }
30});
31
32// journalEditor.controller.ts
33function JournalEditor(...) {
34    this.onLoad = ...;
35}
36
37// journalEditor.provider.tsx
38export const JournalEditorProvider = (props) => {
39    const injected  = useInjectedBindings(...);
40
41    return <JournalEditor {...props} {...bindings} />;
42};
43export angularJournalEditor = react2angular(JournalEditorProvider, ['id', 'document', 'isEditing', 'placeholderText']);
44
45
46// journalEditor.tsx
47export const JournalEditor = (props) => {
48    const { ... } = useInjectedBindings(...);
49
50    const { isEditing, document, afterOnInit } = props;
51    const savedHtml = (editor && editor.getSavedHtml()) || document;
52
53    const onLoad = () => {
54        ...
55    }
56
57    return (
58        <>
59            <div id="journalEditorToolbarContainer" />
60            <div className="flexRowContainer" ref={editorRef}>
61                {isEditing &&
62                    <div id="journalEditor" />
63                }
64                {!isEditing &&
65                    <ContainerWithHTML
66                    className="fr-view"
67                    id="specJournalEntry"
68                    isBlock={true}
69                    content={savedHtml}
70                    />
71                }
72            </div>
73        </>
74    );
75};
76
77// containerWithHtml.tsx
78export const ContainerWithHTML = (props) => {
79    const { content, isBlock, ...rest } = props;
80    if (isBlock) {
81        return <div {...rest} dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(content) }} />;
82    } else {
83        return <span {...rest} dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(content) }} />;
84    }
85};
86    

In the above example, you can notice we have a more complex starting point. The journalEditor component is a placeholder for a 3rd party WYSIWYG library. In this example there are three takeaways.
The first item to note is regarding the multiple dependencies and callbacks. In the Angular version, most of these injections were done via the controller as this was responsible for fetching any dependencies and creating all the required callbacks as well. Considering all the above tips, in the React version we split this into multiple files. The provider will inject any external dependencies and listen to any changes in the flux stores, while passing everything to the component itself. This will help us do any changes down the road in regard to libraries that we use.
The second point to make in this example is the ng-bind-html directive that allows injecting html content directly inside an element. As this is a common scenario in our application, we have created a separate component that uses the React version of injecting html. This is a good example of global component that we publicly announced during our meetings so it can be used at the application level.
A third takeaway is the sq-resize-notify directive. This is used as a resize watcher that will allow us to notify the WYSIWYG library that the size of the editor itself has changed and it should adapt the UI to the new size. As this is again a functionality that is used in several places in our application, we have switched this to a react hook. This way this functionality is easy to inject, as the old angular directive was.

There’s one more example that should be discussed, mostly due to the performance hit we took once we switched to react. When using Angular, the changes in the watched values are done in a digest cycle and multiple changes can be handled in a single cycle. In React, every single prop change will trigger a new execution of the functional component. Consider the following react code example:

1export const Chart = (props) => {
2    ...
3    chartInstance.redrawChart();
4    return (
5        <div id="_chartContainer_"></div>
6    );
7}
8    

In the above example, any change to the props will cause the redrawChart function to be called again. If there are multiple fast changes to the props, the redrawChart will take a lot of CPU time. The solution for this is to actually throttle/debounce the redrawChart function (lodash _.throttle/_.debounce can be used). This would allow us to handle multiple fast changes on the props with a single redrawChart.

All of the above examples are a quick view over the conversion steps we’re doing and the result. Once we have finished with the most basic components, we’re moving upwards in the tree converting larger components and containers, eventually starting to integrate the proper routing and store solutions.

As you can see, by following a set of guidelines and always thinking of the desired end-result, large-scale application conversions are possible. For us, it was a matter of finding the proper balance that will allow us to do all the changes while still delivering new features, handling any outstanding bugs and keeping the high performance.

Written by

Adrian Apostol

Senior Software Engineer

Share this article on

We create value through AI and digital software engineering in a culture of empowerment that enables new perspective and innovation.

OFFICE

24 George Barițiu street, 400027, Cluj-Napoca, Cluj, Romania

39 Sfântul Andrei street, Palas Campus, B1 block, 5th floor, 700032, Iasi, Iasi, Romania