Hotjar is a tool that helps people understand how their users are behaving on their site, what they need, and how they feel. You can find out more about Hotjar and the services we provide at hotjar.com.
From a technical perspective, we provide a rich single-page application (SPA) for displaying data collected from our user’s sites and data provided by their users. Our application was initially written in AngularJS as far back as early 2014. As a bootstrapped startup, the first version of the application was created by a very small team of full-stack engineers. As the company matured, we switched to more specialised roles and now we have a dedicated team of 26+ Frontend Engineers and continuing to grow.
Reasons to migrate away from AngularJS
AngularJS is an older framework that didn’t keep up with modern development practices. Features like lazy loading aren’t very easy to implement and require modification to the application to get it working.
AngularJS is reaching its end of life and will no longer get support.
Due to the framework reaching the end of life it is becoming progressively harder to find developers with recent experience in this framework, due mainly to the two points above.
A lot of lessons were learned from the implementation of AngularJS and these problems were addressed in other frameworks. We want to leverage these better design patterns to make our application scale and easier to maintain.
Why React?
We discussed several framework options including Angular, Vue, Ember, and React.
Angular didn’t feel like the right fit for most developers in the team despite there being a more defined migration path for AngularJS -> Angular. The reason it didn’t feel like a good fit for us as a team was that we felt that the opinionated nature of Angular didn’t align with our goal of allowing teams autonomy in how they develop features and that it would be too restrictive.
Vue was still somewhat new at the time, and no one in the team at the time had any experience using Vue.
Ember is a powerful framework but, as with Vue, no one had any experience using it.
We had developers who had used React in the past and were familiar with the design patterns used in React. Given React’s popularity and community, we wanted to leverage this for both JS libraries and for the pool of developers that we could hire from that already had lots of experience with React.
Our React setup
We opted not to go for a full Redux app setup since we wanted to keep the state local and avoid oversharing the state between different parts of the application if it wasn’t needed, this encourages teams to work independently from each other. We preferred to pull the state up the component tree when needed.
We use TypeScript with React as we find it adds an extra layer of safety to our development. It takes longer to set up components but the payoff exceeds the extra work. We however do have problems with the React/AngularJS boundary since the AngularJS code is not in TypeScript. This means we lose our type safety when passing data to and from the AngularJS application.
We use react-query to handle caching of API requests to avoid over-fetching of data from the backend which, in a way, acts as a global store. Since the query cache is immutable and all changes trigger updates within components we need to worry less about the state being modified in an unexpected way by some other part of the app.
Complete rewrite vs Incremental migration
Complete rewrite
PROS:
A complete rewrite is great because you can ship a new shiny application to your end-users when you finish.
You can develop the application with a clean architecture since you don’t have to carry around any baggage from the legacy application.
CONS:
You have to either halt the development of new features to your customers, or you need to develop features twice so that the new application keeps feature parity with the older one.
You are more prone to introducing regressions and bugs since you are writing tests from scratch and don’t have existing test suites you can leverage to ensure that flows continue to work as expected.
Micro frontends may have solved some of these issues. However, using micro frontends within the AngularJS app isn’t trivial and would still have required rewrites of entire pages or sections of the application. This would still require halting the development of new features while the migration happened.
It is hard to estimate the size of a complete rewrite since there are usually a lot of unknowns.
It usually comes with a huge initial cost of designing the architecture of the application and making application-wide decisions before we even start coding; then when the plan meets reality, it needs to be adjusted and you either end up with an inconsistent codebase or rewrite parts over and over again; this might be the case for the alternative approach as well, though.
Incremental migration
PROS:
You can develop features at the same time as code is migrated to a new framework. This is great because customers continue to get new value.
You can leverage existing test suites such as end-to-end tests to ensure that features still work as expected.
It is easier to size the work needed to migrate since migration work can be broken down into much smaller defined tasks.
It gives you time and opportunity to adjust the approach or architecture over time, evaluate how your ideas work in practice, and change them along the way; it’s still possible with a complete rewrite but it may be more difficult there; with incremental migration, the initial cost is pretty low and you’re not even required of making decisions beforehand - you do it only when you really need it.
CONS:
You carry around a lot of baggage and dependencies from the old codebase as parts of the application may still depend on code that lives in the old codebase; this may have a performance cost for the end-user.
You can’t easily implement new designs into your code since it needs to match the rest of the legacy application.
It takes significantly longer to migrate this way since we have to introduce some workarounds to get data passed between the different frameworks.
We chose to take the incremental approach since at the start of the migration we didn’t have a very big frontend team and we wanted to be able to continue delivering features to our customers.
Despite the drawbacks of incremental migration, we feel like this was a good choice for us as a company and that it has paid off in the long run.
React-2-angular
To approach an incremental migration we needed a way to embed react components within the AngularJS application. We found the great library react-2-angular that lets us create React components that can be embedded within an AngularJS page. This also allows us to pass in dependencies to the react components as props so we can leverage our existing AngularJS services to pass data and API function calls to our react components.
Migrating UI First and Design Systems
It was decided to first attempt to start migrating the UI layer of the application to React while keeping state and some UI logic in AngularJS. This meant we didn’t have to migrate some of our really large AngularJS controllers that had grown over the years and never been nicely split into smaller components. It also had the benefit of allowing us to build pure(ish) components that largely didn’t handle any state, except for maybe state used by controlled inputs. This, in turn, enabled us to get the UI of the app to a point where it is more easily modified across the app while teams work on migrating the controller and service layers of the application.
At a certain point in the migration process, the company decided to create a Design System implemented in React that would standardize all the common UI patterns used through the app. This made the migration of the UI layer even easier since large sections of the UI could be constructed using components from the Design System.
Up until this point, teams had been building reusable components within the project and we didn’t want to throw these away, so these components we used to identify common patterns. We were also able to take these reused components and copy them into the Design System and give them clearly defined type definitions and make them consistent with the Design Language