Migrations
~ 13 minute read
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:
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:
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
Senior Software Engineer
Share this article on