As Angular 1 web applications continue to grow, scaling the UI and adding increasingly complex features tends to get challenging quickly. Depending on the use case and amount of information required to be processed, this can lead to performance degradation, troubles with interactivity, rendering slowdowns and race conditions if those complexities aren’t handled properly (and sometimes even if they are).
Why Develop in React?
This post wasn’t written with the intent to help decide between migrating to a newer Angular framework versus a React.js based application. Both paths provide a multitude of improvements including modular component architecture, type checking, and easy to understand component lifecycle. While Angular is the more opinionated of the two – providing an entire framework to help you structure your app – React lets you integrate it more easily with whatever architecture structure you already have in place, taking a more library-like approach to help ease the transition.
You Might Not Have To
It's important to stress that sometimes a full web app migration might not even be necessary. Angular 1 is a powerful framework that shines in many aspects, so it may not be necessary to change everything in your application, just the pain points. Focusing on speed of rendering, React.js makes use of one-way data flow, virtual DOM and JSX along with the aforementioned improvements to scale the complexities found with UI as your application continues to grow. This makes it an attractive choice to render large and complex elements with. Which also means it does not have to encompass the whole app.
Web Application Migration Approach
The Big Bang
There are usually two main ways to migrate a web application: doing it all in one go (what most people, including myself, refer to as the Big Bang approach) or taking a more methodical approach. Many agree that if the original application is small enough in scope, the big bang approach is doable. This means having developers work on creating the new application alongside the original, while minimizing new features to give time for the new application to reach the original’s functionality.
While application size matters, it is only a part of the formula. It’s not so much about application size, but the ratio of available resources against the effort of application migration. If there are enough developers available with knowledge from the original application to work on the new architecture while still maintaining the old source code, you could make it work. Ideally this would allow fat trimming in the process and allow the rethinking and improvement of solutions. However, having the time and resources to accomplish this is rarely the case.
A Better Option
The Strangler pattern is a very methodical approach to modularizing a monolithic architecture and overtime, converting it. Instead of expanding the monolith with a new feature, add it to the new architecture. Any other dependencies this feature has on the older framework can be ripped and converted alongside the feature. Ideally these dependencies are small in scope and loosely coupled to the main application. Gradually, as more and more pieces are modularized, the monolith will shrink in size, while the new architecture will grow, strangling the old application. In our case, we want to slowly migrate an Angular 1 application to React.
In web app development, a common strangler growth approach is through event interception. Once you have functionality on the new architecture, you can start to slowly change other parts of the application it touches by tapping into the event stream of the old application, continuously ripping those out of the monolith. These event streams can mean any kind of abstraction in the application that is required for state changes within a component. Most of the time these are handled in the data flow or routing layer.
How to Migrate Your Web App from Angular 1 to React
Start small — ideally with segregated parts of your applications. If the developers handling this task are not fully experienced with using React, tackle simple UI components that do not have a lot of dependencies. Functional programming experience and getting familiar with concepts like one-way data flow, modularization, and presentational vs container components can go a long way in building things with React. Remember that not everything might need to change so unless you’ve already decided to get to a full rewrite, I’d focus on the pain points of the applications that can take advantage of React’s speedy and modular approach.
React Component Architecture
Most approaches to implementing React in an Angular 1 application are very similar in nature, they take advantage of its abstraction layer. Using directives helps the team follow best practices as well as start developing modular components. Take this example of a simple stateless react component.
import React, { PropTypes } from 'react';
const myComponent = ({ myProp }) => (
<div>
<h3>This is a stateless React.js component</h3>
<p>{myProp} is a string being passed in.</p>
</div>
);
myComponent.propTypes = {
myProp: PropTypes.string,
};
export default myComponent;
If you are considering only migrating pain points or perhaps just the view layer, libraries like ngReact can be used. However, when targeting the entire application, finer control of the directives rendering out React.js will most likely be needed. Here is what a directive made to render out a react component might look like, passing in a single property.
import React from 'react';
import ReactDOM from 'react-dom';
import myComponent from './my-component.jsx';
angular.module('myModule')
.directive('myReact', function () {
return {
link: link,
restrict: 'E',
controllerAs: 'ctrl',
bindToController: true,
controller: 'myController',
scope: {
myProp: '=',
},
};
function link(scope, element) {
var props = {
myProp: scope.myProp,
};
// render the react component
var reactElement = React.createElement(myComponent, props);
ReactDOM.render(reactElement, element[0]);
// watch to unmount react component
scope.$on('$destroy', ReactDOM.unmountComponentAtNode(element[0]));
}
})
.controller('myController', function () {
this.myProp = 'someValue';
});
...
// render the component in a template
<my-react my-prop="ctrl.myProp"></my-react>
Injecting $scope into a controller will end up in the same result, and myComponent is being imported in from a file somewhere in your folder structure, but that doesn’t mean it has to be done that way. A factory could be used to create React components and share them across the app through dependency injection.
Try to keep one directive and controller per React component. The idea behind this is that components that are tied in to the rest of your application become container components. Start creating presentational components within your containers and pass in the necessary properties from the top level components (containers) down to your presentational ones. This will minimize the number of controllers you need to create for the React sections of the application, which in with the concept of modularization and one-way dataflow.
Take the following as an example. The controller was changed to more closely resemble data that you might get back from an API.
/* angular controller */
...
.controller('myController', function () {
this.countryList = ['Australia', 'Canada', 'China', 'Peru'];
this.users = [
{ name: 'John', countryId: 1 },
{ name: 'Samuel', countryId: 3 },
{ name: 'Raul', countryId: 4 },
{ name: 'Mark', countryId: 2 },
];
});
Below are the container component passing the data down to presentational components.
/* container component */
import React, { PropTypes } from 'react';
import UserListItem from './user-list-item.jsx'
const myContainer = ({ countryList, users }) => (
<ul>
{
users.map((user, index) => (
<UserListItem
country={countryList[user.countryId]}
name={user.name}
/>
))
}
</ul>
);
myContainer.propTypes = {
countryList: PropTypes.arrayOf(PropTypes.string),
users: PropTypes.arrayOf(PropTypes.object),
};
export default myContainer;
/* presentational component */
import React, { PropTypes } from 'react';
const UserListItem = ({ country, name }) => (
<li>
<div>
User: {name}
</div>
<div>
Country: {country}
</div>
</li>
);
UserListItem.propTypes = {
country: PropTypes.string,
name: PropTypes.string,
};
export default UserListItem;
Event Handling
This is probably the most debated part of handling React applications, and unfortunately there is no “right” answer here. Since most event changes in Angular 1 apps are tied to the digest cycle, utilizing the built in tools would make the most sense. But this heavily depends on your event handling architecture around the app, as this can vary wildly if you are using tools like Redux, RxJS, or Flux. Here is an example of handling a change in properties with $scope watchers that are being passed down to the React component.
/* angular directive */
...
function link(scope, element) {
var props = {
myProp: scope.myProp,
};
var reactComponent = element[0];
scope.$watch('myProp', render);
scope.$on('$destroy', unmount);
function render() {
var reactElement = React.createElement(myComponent, props);
ReactDOM.render(reactElement, reactComponent);
}
function unmount() {
ReactDOM.unmountComponentAtNode(reactComponent);
}
}
A bigger challenge is how the React component will interact with the rest of the application. As mentioned above, this is usually when whatever events layer or library that is already in place steps in. Event handlers themselves are treated the same way any other property is, and so the React components themselves don’t have to care too much about what happens with the actions as long as they are being passed down with their appropriate Angular’s services scoped. This is an example of how you could pass an action handler that deals with a service from a controller.
/* angular controller */
...
.controller('myController', function (someService) {
this.onSomeChange = function (value) {
someService(value);
}
});
/* angular directive */
...
bindToController: true,
controller: 'myController',
scope: {
handleAction: '&',
},
};
function link(scope, element) {
var component = element[0];
var props = {
handleAction: function(value) {
scope.$apply(function () {
scope.onSomeChange(value);
});
},
};
...
Scaling
Routing
Eventually to continue growing the new architecture, taking over routes with React will be necessary. Placing the top level containers and their controllers as the route state, alongside a simple html object to define your root component can be a good start. Your React application could eventually move to its own folder structure. From there, fleshing out a working React.js router can be done and the parameters fed in through whatever Angular router is already in place.
angular.module('myModule')
.config(function (
$stateProvider.state('react', {
url: '/some/url/to/react',
views: {
root: {
controller: 'dummyReactController as ctrl',
templateUrl: 'app/templates/dummy-react.html'
}
}
})
Resources
Resource caching and application data storage can be a bit more challenging. They can split and the same approach as before taken, starting with a smaller solution for your React architecture and growing it out as you move more and more functionality in. However, in the off chance that you want to use the same solution from the older application, they can be unified. Using Redux as an example, the store could be passed in to the React application to grab data from. Otherwise, there’s likely no getting around maintaining two different solutions. You might not want to, as creating a unified one might be more effort than it is worth. In the worst case of scenarios, making the React application accessible in a global space (like window or $rootScope) could fix some transition pain points until the new architecture fully takes over.
Final Words
I would like companies brewing over this to keep in mind that a full web app migration is not always necessary. Business decisions and time-to-market for features should take precedence. If the resources are available to make a full migration achievable then the Strangler Pattern approach will make the change feasible. With it, companies can focus on features when necessary (implementing them in the new architecture as much as possible) and start the migration with other parts of the app during downtimes.
Learn More
For more on React, you can check out these informative videos and webinars, as well as previous blog posts. Better yet, inquire about custom training to ramp up your knowledge of the fundamentals and best practices with custom course material designed and delivered to address your immediate needs.