How to Migrate from Angular to React Without a Massive Rewrite
The user interface of Smartly.io’s SaaS application was started as an Angular-based single page application back in 2013. We chose Angular, because it was an all-in-one solution and it had a seemingly straightforward data flow, which allowed rapid development and experimentation. Lately, we’ve been seeking ways to renew our front end stack without compromising our development speed or customer satisfaction.
Smartly.io’s user interface consists of hundreds of components used to manage and optimize various advertising campaigns on Facebook and Instagram. As the product evolved, we added new tools into the mix to ease the build process and keep our development pace up.
Previously, we moved from Grunt to Webpack which allowed us to use eg. ES6 features in our daily work. One of the main concerns of Angular still remained: The state of the UI was spread out to multiple services, directives’ models and such. Using Redux with ngRedux seemed like the correct solution, so we adopted it.
Angular’s relative complexity, rendering performance on large listing views and overall developer happiness were still issues. Many developers at Smartly.io had good previous experiences with React and it was also more mature than Angular 2 at that time. That’s why we chose React as the front end’s component library.
Migrating one component at a time
We had to choose to either rewrite the whole front end application, or go through the migration more gradually. As the first option was too risky and it would’ve stopped us from developing new features for months, we chose the gradual approach.
We wanted to access the Redux state in the new React components, but ngRedux didn’t allow this directly. Our developer Ville made a fix to ngReact which allowed us to create a more general solution to migrate new React components to an existing Angular app without having to rewrite the whole application. This way, the migration started from the outermost leaves of the component tree, and then slowly but surely spread React through the entire application.
Due to the complexity of our application and the size of our code base, it seemed that static typing would improve the maintainability. At the time there were two major libraries for typing Javascript: TypeScript and Flow. Flow supported React out of the box and it enabled us to gradually type our codebase. After careful experimentation, we started introducing Flow to all of our React components.
Generalized bridge binds ngRedux’s state to React
The core of the solution is a generalized bridge component, which allowed us to wrap React components to be used also in the Angular application. The component is based on the excellent reactDirective from ngReact, which handles the basic interactions with Angular, like taking care of the component’s lifecycle, and re-rendering React components when the passed parameters change.
Our bridge service also adds the state from ngRedux to the React component by default, which reduces repetition a bit. The whole bridge code is as follows:
smartlyReactBridge.js:
import React from 'react';
import {Provider} from 'react-redux';const wrapProvider = WrappedComponent => ({store, ...props}) => (
<Provider store={store}><WrappedComponent {...props} /></Provider>
); angular
.module('SmartlyApp.services')
.factory('smartlyReactBridge', function(reactDirective, $ngRedux) {
return function(Component, propNames, directiveOptions = {}, otherProps = {}) {
return reactDirective(
wrapProvider(Component),
propNames,
directiveOptions,
{ store: $ngRedux, ...otherProps }
);
};
});
Binding a React component to an Angular component
A common use case for our users is to find campaigns based on certain properties, such as “has predictive budget allocation enabled” or “has automated post boosting enabled”. Originally, we used an Angular component called CampaignLabels to show the labels.
After porting it to React, we still had to render it in the campaign management view. Unfortunately, the view was still implemented in Angular, so we wrapped the React component in the following way:
CampaignLabels.js:// @flow
import CampaignLabels from './CampaignLabels.js';export default CampaignLabels;angular.module('ui.smartly.campaignLabels')
.directive('smartlyCampaignLabelsBridge', function(smartlyReactBridge) {
return smartlyReactBridge(
CampaignLabels,
[ 'labels', 'showOnlyIcons' ]
);
});
Which is then used in the Angular template in CampaignCtrl:
<smartly-campaign-labels-bridge labels="campaign.labels" />
The wrapped component looks and behaves like a native Angular component. It even replicates one of the more unfortunate things, as it does not complain about missing required properties. Fortunately, the new React components are typed with Flow, which helps in catching missing or incorrectly formatted properties when used directly with other React components.
A big part of the transition was to create a component gallery, which would function as a style guide. Usually, before starting to write new components, the developers scroll through the style guide’s list of components, and see if they could use some of the existing ones instead of creating new ones from scratch. This saved time and effort especially in the beginning of the migration, when there were lots of new components coming in.
We’re 41 % there
Today, more than 41 % of our components are implemented with React and almost all of them have Flow typing enabled. We’re currently replacing the components at the core of the workflow, which will dramatically improve the rendering performance, and also allow us to deprecate a huge number of Angular components. After implementing the base components first, such as forms and interactions, we’ll be able to transform the rest of the application a lot faster.
However, we have to note, that the comparison is not completely apples to apples. The Angular components were designed with a lot broader responsibility than the React components. Therefore, 41 % of components does not equal 41 % of the functionality.
How could we have done this even better?
Our motto for changing the codebase has been “New code? React. Refactoring old code? React maybe”. Our new test setup allows rendering components without the DOM which is a lot faster than always rendering them in the browser, making writing tests a lot easier and nicer. React’s one-way data flow feels more native to all our developers, which has made the migration smoother. Easier testing and the simplicity of the framework had a huge positive impact on the speed of the migration.
In hindsight, we could have paid more attention to these details to make the migration even faster.
- We adopted React even though not everyone was yet up to speed with the previous big change (Redux)
- The whole team was not aligned with adopting React from the beginning
- The reference component with all its bells and whistles could have been created a lot earlier
- Sharing the knowledge and burden of the migration as early as possible would’ve made it faster to complete
- Introducing Flow at the same time added yet another level of complexity, especially since at the time we adopted Flow, it was still missing support for Stateless Components and had some surprising regressions bugs
- A hands-on session for migrating components works better than just a knowledge share. We fixed this later at our Engineer.io workshop day where we ported existing components to React as a pair-coding exercise.
All in all, we’re happy with how the migration has made rendering faster and creating new components simpler — and our users have noticed the results. We’re aiming to finish the migration during the first half of 2017, which should allow us to simplify our frontend library stack nicely.
Want to see how the migration continues? We’re ready to open 100 % of our codebase to you, check smartly.io/developer for details ;)
Originally published at www.smartly.io.