The stress of taking a single brick from the tower and placing it at the top; the mounting intensity with each move made: Anyone who’s ever played a game of Jenga understands this feeling. Imagine playing Jenga with a huge tower in front of a live audience, where any misstep means total disaster. My team’s experience migrating an application from AngularJS to React has felt like a similar challenge.
Imagine a front-end application as the Jenga tower and each brick as an interdependent component. Removing one brick might mean fixing a bug in legacy code, while replacing one brick could be programming a new feature. App users should not notice any change, except a potentially refreshed design, and the app should perform at least as well as before. Since both frameworks are JavaScript frameworks, this shouldn’t be a huge challenge, right?
Truthfully, we’ve actually found this migration to be quite a challenge. Planning the migration, developing new features on our product roadmap, and paying down tech debt is not easy, but we had to simultaneously tackle all of these challenges.
Despite these hurdles, we decided it was time to move from AngularJS to React.
AngularJS marked a new beginning in front-end development, and it showed there’s more to front-end development than just UI (user interface) work. However, the levels of abstraction from AngularJS were still too complicated for most front-end engineers. One could not longer simply write a jQuery function to toggle the visibility of elements—now, proper engineering was needed, from updating the DOM based on state to managing dependencies. (Remember, these were the days before module bundlers.)
Since AngularJS’s release, many UI frameworks and libraries have increased in popularity. The difficulties of AngularJS eventually became cumbersome, so front-end developers opted for newer frameworks. To top it all off, the last version of AngularJS that offered a LTS of three years was 1.7, which released in July 2018. Serious products can’t be built on frameworks without long-term support, so this was a signal that we needed to change frameworks soon.
We chose React based primarily on its adoption among front-end engineers, and the ease of hiring those with React experience. It also performs better than Angular. The React ecosystem is huge, and was already bigger than Angular’s by the time we decided to make the shift..
After the decision to migrate, we had to figure out where to start. We searched for stories of other companies who had migrated, and to our surprise, there was little information out there.. So, we carved our own path.
Front-end engineering is about displaying bits of data that’s optimized in a friendly fashion to users, all while solving their problems. Whether it’s interacting with another service or presenting information about a product, the main purpose of a front end is enabling users to engage with the displayed bits of data. The problem-solving ability of a product can be measured by three factors: (1) the time spent by a user in executing a particular task; (2) the availability of the service; (3) and the consistency of the data. UX (user experience) combines the effectiveness of a product with the feeling the user gets by interacting with it.
If there’s tech debt accumulated in your AngularJS code, you don’t want to bring that over to your new React codebase. It’s also unwise to port bugs that haven’t been resolved. To start fresh, you need to begin with the basics—the architecture.
To start developing a proper architecture, we needed to wrap our minds around the differences between developing an app in AngularJS versus React. The first big issue we tackled was the fact that AngularJS has vendor lock-in, which React does not. (React’s tagline is “A JavaScript library for building user interfaces.”) In Angular-land, you have to follow the Angular way and use it’s tooling for API calls, state, and more. You could choose not to, but you’d run into problems like updating the UI if the data received didn’t happen in the same call stack (e.g., callbacks, Promises, etc). Also, there is a lot of technical jargon to learn with Angular, such as controllers, directives, modules, services, etc. To effectively manage all this, the AngularJS way recommends offloading state management operations to the controller. State is fetched by services. Controllers are the glue which retrieve state from a service, manages the state and update the view. The view in AngularJS is comprised of templates which display the current state as reported by the controllers.
In React, the most important thing to understand is data flow. It’s sufficient to picture a React application as a data tree in which each node is a component (or Jenga brick). Each component has state which is managed by its respective component and receives properties known as props, which are managed by a parent component. This system regulates how data flows in the application. The props represent the edges between nodes, whereas the state encapsulates data and is represented by the nodes themselves. The state can be passed down in the tree through props or used for that particular component. Alternatively, one can create “context,” which creates shared data across nodes that are subscribed to that context.
If you want to retain Angular’s architectural style, you can use analogous design patterns in React. For example, you can create different stateless components that are similar to the Angular templates, but which merely display data and handle events. To replace Angular controllers, you can use a stateful component that manages the data received either by a server, context, its local state or props from upper components and supplies these to multiple stateless components.
If you want to do things the React way, you can enhance your application with a centralized state container library, such as Redux.
Now we know the differences between how each framework works, but are there commonalities we can leverage to make our migration easier? In modern web applications, there are often three layers, all of which are packed within the same bundled file: the state layer, which mostly handles communication with the server and acts as a caching layer; the controller layer, which manages state transitions and communication with the state layer; and the view layer which displays the current state of the application.
To plan a migration you have to consider all three layers of the application. This is important especially when migrating an application that was not efficiently architected; it’s a good place to start doing things right. We considered three different approaches to migrating an AngularJS application:
Let’s consider each of the aforementioned approaches.
This approach focuses first on replacing AngularJS services. To this end, a state container like Redux, Flux or MobX is a good way to centralize all data operations and decouple your state layer from any other layer. Make sure to pick the one that best suit your needs -- there are plenty of resources that describe each state container. By centralizing all store operations and offloading them from AngularJS, we also achieve full freedom for the UI. You avoid being locked-in to a UI library, making it fairly easy to replace it with whatever you need. In this case, we can just replace AngularJS controllers, directives and templates with React components until we finally remove all AngularJS from our codebase.
This approach focuses on AngularJS templates first. Since AngularJS templates are mostly HTML and CSS, it’s fairly easy to put that into a React component by using appropriate bindings to connect your React component to your Angular controllers. Libraries like react2angular or ngReact let you bind React components directly to your Angular templates. You may run into performance issues, but all in all, the performance is not significantly affected. With regard to a decoupled architecture, it’s still possible to achieve it this way, since it’s just inverting the migration order of the first approach. The controllers are the glue so either way that must stick in either of both cases to the later step. The latter step is the same as the first step of the first approach we presented.
The third and last approach that we consider focuses on parts of the application and aims to migrate them vertically. You can picture it as follows: pages that are rendered independently, will also be migrated independently. It’s tempting to go for this approach, but it’s also very easy to design your system in a way that is going to be continuously churned. Also, performance can be affected for as long as the migration is ongoing. When migrating a rendered view, there are hidden state uses that might not be considered, and in the end that requires maintaining state in two different parts of your application.
We actually used all three approaches in our migration. Our initial steps were migrating very small components following the second approach, i.e. we were still using Angular’s controller and state layer, but we implemented the UI in React. We concluded that this way, we were prolonging paying down tech debt what eventually could be counterproductive. The opportunity to use this chance to do things right was misused. We learned from it, though. We had first-hand experience with libraries that later on would be reused in the other approaches we took.
We re-evaluated and went for the third option. We migrated our UI on a per-rendered view basis. We spent on average one month per full view, including rewriting all interactions, migrating the state to Redux and so on. We made progress, and were able to bring one app to the another without too much hassle. However, we realized that this approach was taking too long. Our delivery time was being compromised and it was increasing the risk of poor code quality due to time pressure. Thus, after migrating two full pages, we stopped and re-evaluated again.
We wanted faster delivery times and the ability to scope our steps so that our engineering resources could be split across different sprints without losing momentum. Also, we wanted to use this chance to do things right and pay down tech debt as we went along, or at least not carry tech debt over into our newer codebase.
In general, we were happier first migrating state, and we believe it will become increasingly important when we start migrating our most important and data-heaviest component. Remember: frontend is about displaying bits of data. So by making sure the data is not framework-dependent, you can start fully focusing next on optimizing the view for your users for the most effective way of solving the problem.
We’ve learned some lessons which are helpful to anyone migrating an application from AngularJS to React, and indeed to anyone performing any kind of front-end migration. Here are some of the most important ones:
Without architecting your application correctly, it is easy to migrate the tech debt, as well. Although the time investment is already high, we recommend resolving tech debt during the migration, instead of carrying it over, as the future costs of resolving tech debt are even higher. Start by understanding the basic architecture of a React and an AngularJS application and then which approach to follow.
Migrating an application gives you a chance to correct whatever was wrong. Don’t waste the opportunity and help your team plan the product development accordingly. It may take more time, but a good refactor will pay its time investment back as your product grows.