How to migrate an application from AngularJS to React and Redux

22,831 reads

@ viniciusdacal Vinicius Dacal Software Engineer, remote worker. Loves creating, sharing and learning.

Starting this year, I was hired by BEN Group, with the main goal of helping them migrate a legacy application from AngularJS to React and Redux. Since then, we have been creating solutions inside the project, that is working greatly so far.

reactions

In this post, I intend to show the main approaches we followed and share some solutions we created, to allow us migrate the project gradually and without loose our sanity.

reactions

Disclaimer: Our focus here, is not refactoring legacy code, but remove it as soon as possible. We avoid solutions that takes too much time or focus in changing the legacy code in order to let it “prettier”. That said, we prime to write new code with great quality.

reactions

Move the build to webpack.

reactions

This step, I consider the most import from the whole process, once with Webpack, you can start using the instruction import to get your dependencies and modules and you can start getting rid of Angular’s Dependency Injection(DI). This is also necessary to start writing React code in the application.

reactions

If you use Angular’s template cache, Pug(Jade) or any other thing that influences the build, don’t worry, Webpack will have a loader for each one of them. Don’t forget to let your Webpack configured to transpile ES2015 and JSX.

This step doesn’t focus on moving all the DI to imports, but instead, make your build work with Webpack. It’s important to keep that in mind, to avoid staying in this task for weeks and cause conflicts in dozens of files.

reactions

In AngularJS, normally, the build process gets all the dependencies you need from node_modules and insert them in the bundle. We need to keep that behavior in the new build as well.

reactions

You need to consider the legacy code as an enemy to be defeated. We need to act with caution and we need to be strategic. This also means, that in certain moments, we need to do things that aren’t pleasant.

To solve that matter, we created a file

vendor.js

reactions

require ( 'angular' ); require ( 'angular-resource' ); // ...other dependencies

, and imported all the dependencies inside it:

Most of the dependencies registered themselves globally in the window object, when they are imported. So, we only need to import them as the above example. Although, some of them doesn’t do that and we need to do it manually. Bellow we have an example of what we had to do with moment and jQuery:

reactions

window .moment = require ( 'moment' ); window .$ = require ( 'jquery' ); window .jquery = window .$; window .jQuery = window .$;

This practice could be weird, however, you need to consider that most of the dependencies are relying on

window.$

onwindow.jQuery

window.jquery

reactions

, othersand others even on

After creating the vendors file, import it in the entry point of your application and this way, all your dependencies will be in the bundle:

reactions

require ( './vendors' );

Another step, is to ensure that all your application’s files are in the bundle. The ideal, is having each module with an index file, importing controllers, factories, views, etc.. Having that, you only need to import those indexes in the application’s entry point, same way you did with vendors, as the following example:

reactions

require ( './vendors' ); require ( './app/common/index' ); require ( './app/core/index' ); require ( './app/layout/index' );

If you don’t have the indexes, you can try to follow a solution a little dare, not much advised though. That would be find a regex to match all your files and import them using require.context, as the below example:

reactions

function requireAll ( r ) { r.keys().forEach(r); } requireAll( require .context( './app/' , true , /\.(js|jsx)$/));

The above code, will force Webpack to include in the bundle, all

.js

.jsx

/app

.test.js

.spec.js

.stories.js

reactions

andfiles that are insidefolder and its subfolders. If you decide to follow this way, don’t forget that you may haveand evenfiles, and you will have to exclude them in the regex.

Also, remember that in some cases, Angular is counting on the ordering that your files are loaded, so, this solution could end up not working at all.

reactions

When you finally get your build working, hurry up to create a pull request targeting your master branch. Apart React, moving the build to Webpack is already a gain for your application. The Angular’s DI makes the application to be strongly coupled and Webpack is our ally against that

reactions

Render React components inside AngularJS

reactions

The second most important step, because without that is not possible to migrate gradually. The idea here, is that you could use React components inside Angular, as they were directives. To achieve that, we are currently using ngReact in our project.

reactions

The ngReact repo is advising to use the lib react2Angular. However, we are using Angular 1.5.8 in our app, and we end up getting some problems trying to use the other lib. I already used react2Angular in another project, that were using a more recent Angular’s version and I didn’t have any issue. That said, ngReact even not being updated anymore, has all the feature we need to transform our components into directives. My advise is: choose the lib that works for you and go ahead, both are very similar

To integrate ngReact in the project, you can install it from npm:

reactions

$ npm i --save ngreact

reactions

And then import it in your vendors:

reactions

require('

ngreact

');

reactions

You also need to install react and react-dom in your project:

reactions

npm i --save

react

react-dom

reactions

And then, register react module into Angular:

reactions

angular.module('app', ['

react

']);

reactions

With that done, we can create a Button component, as we would create in a regular React application:

reactions

import React from 'react' ; const Button = ( { children, ...restProps } ) => ( < button { ...restProps }> {children} </ button > ); export default Button;

And then, we define a directive that works as a wrapper for Button:

reactions

import Button from 'path/to/Button' ; const props = [ 'children' , 'id' , 'className' , 'disabled' , 'etc..' , ]; const ReactButton = reactDirective => reactDirective(Button, props); ReactButton.$inject = [ 'reactDirective' ]; export default ReactButton;

In the directive’s file, we must define the name of all the props whose are used by Button, in order to ngReact understands what it should pass down to the component.

reactions

Directive defined, we need to register it in Angular:

reactions

import reactButton from 'path/to/react-button' ; angular .module( 'app' ) .directive(‘reactButton’, reactButton);

The Angular’s modules that you gonna use to register it doesn’t matter, just make sure it was registered in the application.

reactions

Once registered, we can now use the directive in any angular’s view, as the follow example:

reactions

<div>

<

react-button

class-name="btn"></

react-button

>

</div>

reactions

Notice that here, instead of CamelCase, we use dash to split the words. In this case, reactButton becomes react-button and

className

class-name

reactions

, becomes. It’s important to keep that in mind, given this is a common mistake and could take hours to debug.

It’s common to use ngReact to render small components inside AngularJS applications, but not much productive though.

reactions

Angular UI Router, allows us to pass a parameter template in the route config. Exploring that, it’s possible to create a wrapper component for each application’s screen and then use those wrappers as the following example:

reactions

$stateProvider.state( 'user.login' , { url : '/login' , template : '<react-screen-login></<react-screen-login>' , });

In the above example, we define a login route and pass it to a component, which is the whole Login screen. This way, we can migrate a whole screen per time, instead of migrating component by component.

reactions

My gold advise here, is to install Storybook in the project, to create and test the small components. That way is easier to build solid components and then put them together into the screens.

reactions

Screens: Also known as pages, they are the root component of each route.

Share dependencies

Define an entire screen is amazing. However, when we come to this point, we also need to share some Angular dependencies with React.

reactions

In the case of BEN, the dependencies we need, was only ready after the Angular’s initialization, after it have executed its providers, config, etc... Given that, it wasn’t possible to export them using the export keyword. To go around that, we created an object and a helper function to inject the dependencies. To implement that solution, we only need to create a file named

ngDeps.js

reactions

export const ngDeps = {}; export function injectNgDeps ( deps ) { Object .assign(ngDeps, deps); }; export default ngDeps;

with the following code:

We call injectNgDeps inside an Angular’s run process as the below example:

reactions

import { injectNgDeps } from 'path/to/ngDeps' ; angular .module( 'app' , []) .run([ '$rootScope' , '$state' , ($rootScope, $state) => { injectNgDeps({ $rootScope, $state }); }, ]);

We do that because we want to have access to the dependencies as soon as possible and run is one of the first processes executed in the initialization. The injectNgDeps accepts an object as argument, and merge it with the ngDeps object.

reactions

When you need any dependency inside a React component, you only need to do as the following:

reactions

import React, { Component } from 'react' ; import ngDeps from 'path/to/ngDeps' ; class Login extends Component { constructor (props) { super (props); const { $state, $rootScope } = ngDeps; this .$state = $state; this .$rootScope = $rootScope; } render() { return < div /> } }

Notice that the first thing we do, is to import ngDeps. If you try to access ngDeps.$state right after the import, the result will be undefined, because the run process didn’t ran yet. For that reason, we access the value inside the component’s contructor method, because the components will be instantiated only after Angular has initialized.

reactions

We extract the dependencies from ngDeps and we assigned them to the object this, because this way we can access this.$state inside any class’s method

reactions

This way it’s possible to share any Angular’s dependency with React components. However, use ngDeps with parsimony. Keep always in mind: Can I export this dependency using export? If the answer is yes, you always chose to use export, otherwise you use ngDeps.

reactions

Another thing to highlight, is that it is important to keep the access to ngDeps restrict to the top components in the tree. That means screens, and possibly some containers. And then, pass down to children by props. This way it will be easier to remove ngDeps when the time comes.

reactions

Integrate Redux in the application

After solve the question about sharing dependencies between both sides, we can go ahead and integrate Redux into the application. To do that isn’t so hard, but there are some particularities though.

reactions

First, configure the store following the docs instructions, as you would do in any application. However, once you create the object store, you must export it asthe following way:

reactions

export

const

store

= createStore(rootReducer);

reactions

That will allow us to access the store object in other files in the application

reactions

In a regular application, we integrate our containers to the store, using the method connect from react-redux. Although, that only works because we insert the Provider with the store as the root component in the application, as we can see in the lib’s docs:

reactions

ReactDOM.render( < Provider store = {store} > <MyAppRootComponent /> </ Provider > , rootEl )

The problem is that we can’t have a single root component in our application, we have many. It’s impractical that we keep controlling that manually, which components should contain the Provider and which doesn’t. To solve that, we created a High Order Component, which abstracts that logic and insert the Provider as a wrapper when necessary. To make it accessible, I published it on Github and on NPM as redux-connect-standalone.

reactions

To install it by NPM:

reactions

npm i --save redux-connect-standalone

reactions

And then, we can create our connect file and use the following code:

reactions

import createConnect from 'redux-connect-standalone' ; import store 'path/to/youStore' ; export const connect = createConnect(store);

Inside your components, instead of importing the connect method from react-redux, import it from the file you just created. And use it the same way you would use the original method:

reactions

import { connect } from 'path/to/youConnect' ; export default connect( mapStateToProps, mapDispatchToProps )(YourContainer);

As we are respecting the same signature from the original method, the day you have a Provider as your application’s root component, you will only need to execute a search replace in the import method, to replace it by:

reactions

import { connect } from 'react-redux' ;

If you use or intend to use redux-form in your application, I also created and published a HOC to reduxForm method, the redux-form-connect-standalone. His usage is very similar to the HOC we saw above.

reactions

Final words

Having those recipes in hands, it’s possible to migrate your application gradually. However, there are always other complex stuff that shows up when you are migrating an application’s base technology. It’s important to keep in mind that all the solutions above are a middle ground between Angular and React. The final goal is to get rid of all of them and use the React and Redux’s conventions and good practices. So, whenever you create a solution, think how hard will be to get it removed later.

reactions

If you find any interesting solution or problem, share with us.

reactions

If you like this post, help us spread the word for more people to keep evolving and improving their applications.

reactions

Share this story @ viniciusdacal Vinicius Dacal Read my stories Software Engineer, remote worker. Loves creating, sharing and learning.

Tags