DEV Community

Cover image for From AngularJS to React Bit By Bit
Anastasia Kaplina
Anastasia Kaplina

Posted on • Edited on

From AngularJS to React Bit By Bit

Summary:
There are certainly benefits to migrating your application from AngularJS to React (frankly to any other modern framework/library). If a full rewrite is not for you, in this article I will walk you through a simple way to introduce React into your AngularJS application. Even if you end up using an open source library for bridging AngularJS to React, this article will give you understanding of its underlying principles.

Want to skip all the explanations? Jump directly to the full working example.

So, you decided to switch your application from AngularJS to React. Good! Because frankly you should run from that no-longer-supported framework towards anything else. Any modern framework/library is more performant, easier to work with and has a larger community.

Reasons

At Awesense we have two use cases which are hard to implement with AngularJS but super easy with React:

  1. Dynamic content. We wanted to give users the ability to customize their dashboard page. React elements and their properties are just JS classes, functions and objects, and you don’t need to do anything special to simply map user configuration to the correct UI.

  2. Map overlays. The Awesense client application is map centric, and we need to render various UI elements from vanilla JavaScript. With React you can create root components whenever you want, whereas AngularJS was designed to be bootstrapped once and take care of everything in your app. Jumping in and out from the AngularJS universe is possible but definitely not as elegant as one line of code in React.

A full rewrite is rarely a good decision. Migrating gradually allowed us to spend more time on AngularJS tech debt during quieter periods and ramp up feature development to support business growth when it mattered, a good balance that everyone was happy with.

You can use libraries like ngReact, react2angular, angular2react, to help you with the migration, but it takes very little code to implement your own solution, and it’s good to fully understand how it works anyway. The Awesense solution was inspired by this Small Improvements blog post and their open source example.

Initial Steps

In order to make the transition smoother you should first prepare your AngularJS codebase with the following steps:

  • Define your controllers and component templates in the same file if you are not doing so already.

  • Start using AngularJS components instead of directives. Components provide lifecycle hooks. Although React and AngularJS lifecycle methods are called at different times in a component render cycle it’s beneficial to familiarize yourself with the concept.

  • Divide your components into container and presentational components. Such separation of concerns makes your code easier to manage and reuse.

  • Embrace unidirectional dataflow architecture: stop using the = two-way-binding, pass inputs to child components with < binding instead. Treat your child components as pure functions that don’t mutate passed arguments. Instead, children should update parents’ state by calling callbacks passed down to them as outputs. This will give you better visibility into how the data flows through your application, where it’s updated, and who owns it.

Components

Our strategy is to start the migration from “leaf” presentational components, work your way up to stateful components, and ultimately to the top-level components which are rendered in routes. That way you don’t ever need to load AngularJS code inside a React component, and you don’t need to deal with routing until the very end.

Simple Component

First, you need a way to use React components inside your existing AngularJS code. I won’t cover how to use AngularJS components from inside React components since we don’t need that with our strategy, and our ultimate goal is to switch away from AngularJS anyway.

Create a simple React component:

import React from 'react';

export default function ReactExample()  {
  return <div>Hello world</div>;
};

An equivalent AngularJS component would look something like this:

angular
  .module('myModule', [])
  .component('reactExample', {
    template: '<div>Hello world</div>',
    controller:  function() {
      // component logic
    }
  });

So, we need a helper function which would wrap our React component into an AngularJS component that can be used from our old AngularJS codebase:

// ---- angular-react-helper.jsx ----

import ReactDOM from 'react-dom';
import React from 'react';

export function reactToAngularComponent(Component) {
  return {
    controller: /*@ngInject*/ function($element) {
      this.$onInit = () => ReactDOM.render(<Component/>, $element[0]);
      this.$onDestroy = () => ReactDOM.unmountComponentAtNode($element[0]);
    }
  };
}


// ---- angular component file ----

import { reactToAngularComponent } from '<path>/angular-react.helper.jsx';
import ReactExample from '<path>/react-example.component.jsx';

angular
  .module('myModule', [])
  .component('reactExampleBridge', reactToAngularComponent(ReactExample));

Here our helper function reactToAngularComponent returns a simple AngularJS component config without a template. Instead this config accesses the underlying parent DOM element with $element[0] and uses $onInit and $onDestroy AngularJS lifecycle methods to mount ReactExample component on creation and unmount it on destruction of the reactExampleBridge component.

Note the suffix "Bridge" in the reactExampleBridge component name. In the middle of your migration this naming convention will make it easy to identify an AngularJS component that only has bridge component children remaining (which means that we can now rewrite the parent component in React and drop all the bridges).

Now we can use reactExampleBridge inside another AngularJS component template:

angular
  .module('myModule')
  .component('anotherComponent', {
    template: '<react-example-bridge></react-example-bridge>'
  });

Passing Props

Let’s change the ReactExample component so it accepts some props:

import React from 'react';
import { string } from 'prop-types';

export default function ReactExample(props)  {
  return <div>{props.exampleText}</div>;
};

ReactExample.propTypes = {
  exampleText: string
};

We don’t need to make any changes in reactExampleBridge component, but the reactToAngularComponent helper function needs some tweaking:

// ---- angular-react-helper.jsx ----

import ReactDOM from 'react-dom';
import React from 'react';

function toBindings(propTypes) {
  const bindings = {};
  Object.keys(propTypes).forEach(key => bindings[key] = '<');
  return bindings;
}

function toProps(propTypes, controller) {
  const props = {};
  Object.keys(propTypes).forEach(key => props[key] = controller[key]);
  return props;
}

export function reactToAngularComponent(Component) {
  const propTypes = Component.propTypes || {};

  return {
    bindings: toBindings(propTypes),
    controller: /*@ngInject*/ function($element) {
      this.$onChanges = () => {
        const props = toProps(propTypes, this);
        ReactDOM.render(<Component {...props} />, $element[0]);
      };
      this.$onDestroy = () => ReactDOM.unmountComponentAtNode($element[0]);
    }
  };
}

As you can see, we added two more helper functions:

  • toBindings – generates an AngularJS component bindings object out of React component propTypes. We need to use it only once, when registering the AngularJS wrapper component.

  • toProps – creates a React props object from AngularJS controller values. We need to use it every time the controller values change, which is why the $onInit lifecycle hook was replaced with $onChanges. Conveniently, the same ReactDOM render method can be used to mount the React element into the DOM for the first time as well as to efficiently update an already mounted React element with new props.

This imposes some limitations on how you can declare React components and use them in bridge components:

  • All props must be declared explicitly in the propTypes object. Our ReactExample component won’t receive any unspecified props. It’s good practice to have propTypes defined on all React components anyway for documentation purposes. It also makes debugging easier since React outputs warnings in the console when a prop of an unexpected type is passed to a component.

  • All inputs passed to a bridge component must be immutable, otherwise the $onChanges lifecycle method will not be triggered, and the ReactExample component will not receive updated values.

  • All inputs passed to reactExampleBridge must be expressions because the toBindings helper function uses only the < type of binding.

Now we can pass example-text input to our reactExampleBridge component:

class AnotherComponentController {
  /*@ngInject*/
  constructor() {
    this.value = 'exampleValue';
  }
}

const anotherComponentConfig = {
  controller: SomeComponentController,
  template: `
    <react-example-bridge
      example-text=”$ctrl.value”
    ></react-example-bridge>
  `
};

angular.module('myModule').component('anotherComponent', anotherComponentConfig);

Different Types of Bindings

Usually when defining an AngularJS component you would use three types of bindings: <, @ and &. A simple todo list AngularJS component would look like this:

// --- todo-list.js ---

const todoListComponentConfig = {
  bindings: {
    title: '@',
    items: '<',
    onSelect: '&',
  },
  template: '...'
};

angular.module('myModule').component('todoList', todoListComponentConfig);


// --- parent-component.js ---

class ParentComponentController {
  /*@ngInject*/
  constructor() {
    this.todoItems = [ ... ];
  }

  selectItem(itemId, nextState) {
    // update logic goes here
  }
}

const parentComponentConfig = {
  controller: ParentComponentController,
  template: `
    <todo-list
      title="Tasks For Tomorrow"
      items="$ctrl.todoItems"
      on-select="$ctrl.selectItem(itemId, nextState)"
    ></todo-list>
   `
};

angular.module('myModule').component('parentComponent', parentComponentConfig);

However, our reactToAngularComponent helper only uses < type of bindings. Let’s rewrite our todoList AngularJS component as a React bridge to see how to pass different types of bindings to it.

// ---- todo-list.jsx ----

import React from 'react';
import { arrayOf, bool, func, shape, string } from 'prop-types';

function TodoList(props) {
 return (
   <div>
     <h2>{props.title}</h2>
     {props.items.map(item => (
       <label key={item.id} style={{ display: 'block' }}>
         <input
           type='checkbox'
           checked={item.isSelected}
           onChange={() => props.onSelect(item.id, !item.isSelected)}
         />
         {item.label}
       </label>
     ))}
   </div>
 );
}

TodoList.propTypes = {
  title: string,
  items: arrayOf(shape({
    id: string,
    label: string,
    isSelected: bool
  })),
  onSelect: func
};


// ---- todo-list-bridge.js ----

import { reactToAngularComponent } from '<path>/angular-react.helper.jsx';
import TodoList from '<path>/todo-list.jsx';

angular
  .module('myModule')
  .component('todoListBridge', reactToAngularComponent(TodoList));


// ---- app.js ----

class AppController {
  constructor() {
    this.todoItems = [
      { id: '1', isSelected: true, label: 'Wake up' },
      { id: '2', isSelected: false, label: 'Cook breakfast' },
      { id: '3', isSelected: false, label: 'Conquer the World' }
    ];
  }

  handleItemSelect(itemId, nextState) {
    // update logic goes here
  }
}

const appComponentConfig = {
  controller: AppController,
  template: `
    <todo-list-bridge
      title="'Tasks For Tomorrow'"
      items="$ctrl.todoItems"
      on-select="::$ctrl.handleItemSelect"
    ></todo-list-bridge>
  `
};

angular.module('myModule').component('myApp', appComponentConfig);

The items input was originally defined with the < binding type, so we didn’t need to make any changes to it, but for title and on-select we had to make the following adjustments:

  • Originally title was defined with @ binding, so we could pass a string right away. Now for todoListBridge components AngularJS will evaluate the passed title input as an expression, so we need to double quote the string:

    title="'Tasks For Tomorrow'"

  • Originally on-select was defined with & binding and required us to specify what arguments the callback expects. Now we don’t need to do that since we pass the underlying function itself:

    on-select="::$ctrl.handleItemSelect"

    Since the handleItemSelect function never changes we can optimise our parent component by using :: one-time-binding syntax that tells AngularJS not to watch for handleItemSelect changes.

Immutable Data

Let’s implement handleItemSelect logic.

handleItemSelect(itemId, nextState) {
  this.todoItems = this.todoItems.map(item => {
    if (item.id === itemId) {
      return Object.assign({}, item, { isSelected: nextState });
    }
    return item;
  });
}

We are replacing the todoItems array with its copy by using ES6 Array.prototype.map. The todoBridge component’s $onChange method won’t detect the change if you simply update a todo item in place. Therefore the underlying TodoList React component won’t be re-rendered and UI will stay stale.

I strongly recommend getting used to not mutating your data, it makes reasoning about your application state much easier, and prevents many bugs. Having immutable data will also open a door into further optimizations with React via shouldComponentUpdate and React.PureComponent.

Callbacks

Since we’re passing the handleItemSelect callback as an expression, when that function is called in the TodoList component it won’t know that it was originally defined on AppController. For this keyword inside the callback to point to the controller, we can either bind the context to the function with the Function.prototype.bind() method or define the method with a fat arrow function as class instance fields, all of which will bind the right this under the hood.

// binding in the constructor
constructor() {
  // ...
  this.handleItemSelect = this.handleItemSelect.bind(this);
}


// or defining the method with with a fat arrow as class instance field
handleItemSelect = (itemId, nextState) => {
  // ...
};

For all outputs declared with & binding, AngularJS will trigger a digest cycle whenever the callback is called. Now we need to do it manually, otherwise you’ll get rather peculiar behaviour: your UI would update only on the next digest cycle tick.

/*@ngInject*/
constructor($scope) {
  this.$scope = $scope;
  // ...
}

handleItemSelect(itemId, nextState) {
  this.todoItems = this.todoItems.map(item => {
    if (item.id === itemId) {
      return Object.assign({}, item, { isSelected: nextState });
    }
    return item;
  });

  // Need to trigger digest cycle manually since we pass this function
  // to a bridge component and changes to this.todoItems
  // will happen from outside of the AngularJS framework.
  this.$scope.$apply();
}

Services and Factories

AngularJS is a big framework which offers a lot of functionality out of the box. Your final goal is to find a replacement for all of the AngularJS services you use. But until that’s done, your React components need a way to access those services. For that we need another helper function:

function getAngularService(name) {
  const injector = angular.element(document.body).injector();
  return injector.get(name);
}

Add some sanity checks for easier debugging:

function getAngularService(name) {
  const injector = angular.element(document.body).injector();
  if (!injector || !injector.get) {
    throw new Error(`Couldn't find angular injector to get "${name}" service`);
  }

  const service = injector.get(name);
  if (!service) {
    throw new Error(`Couldn't find "${name}" angular service`);
  }

  return service;
}

Let’s add a button to our React TodoList component that scrolls to the top of the list, and use AngularJS $anchorScroll service to perform that scroll:

class TodoList extends React.Component {
  constructor(props) {
    super(props);
    this.$anchorScroll = getAngularService('$anchorScroll');
    this.goToTop = this.goToTop.bind(this);
  }

  goToTop() {
    this.$anchorScroll('title');
  }

  render() {
    return (
      <div>
        <h2 id='title'>{this.props.title}</h2>
        {this.props.items.map(item => (...))}
        <a onClick={this.goToTop}>Go to Top</a>
      </div>
    );
  }
}

A couple tips to make your migration a bit easier:

  • If a service doesn’t have any AngularJS dependencies, don’t register it on your app module. Import it directly to the files where you use it.

  • Hide each AngularJS service in a wrapper that only exposes the functionality that you need. This way you can switch out the underlying AngularJS service much more easily when it’s time to replace it.

Using a Service Outside of AngularJS

Pick an AngularJS service, for example $http. Create a new myHttpService class and get the AngularJS service with the getAngularService helper function. Add only those methods of $http which your application needs. Additionally you can isolate relevant logic that is reused often in your code, such as a custom server error handler in case of $http wrapper.

Finally, instantiate your new service:

// --- http-service.js ---

class myHttpService {
  constructor() {
    this.$http = getAngularService('$http');
  }

  send() {
    // your logic that uses Angular $http service
  }
}

export default new myHttpService();

Such a wrapper can only be imported when the underlying AngularJS service is already registered with AngularJS. A safe way to do that is at component initialization time.

const dependencies = {
  getMyHttpService: () => require('<path>/http-service.js').default
};

class MyReactComponent extends React.Component {
  constructor(props) {
    super(props);
    this.myHttpService = dependencies.getMyHttpService();
  }

  // now you can use this.myHttpService in your React components,
}


// or import myHttpService the same way to some Angular component
class MyAngularController {
  /*@ngInject*/
  constructor() {
    this.myHttpService = dependencies.getMyHttpService();
  }

  // now you can use this.myHttpService in your Angular component,
}

The benefit of this approach is that a wrapper is imported the same way to both React and AngularJS components.

Complete Code

Let’s recall. Here is a complete TODO list example code.

// ---- angular-react-helper.jsx ----

// 40 lines of code you need to start transforming your AngularJS app
// into a hybrid app.
import ReactDOM from 'react-dom';
import React from 'react';

function toBindings(propTypes) {
  const bindings = {};
  Object.keys(propTypes).forEach(key => bindings[key] = '<');
  return bindings;
}

function toProps(propTypes, controller) {
  const props = {};
  Object.keys(propTypes).forEach(key => props[key] = controller[key]);
  return props;
}

export function reactToAngularComponent(Component) {
  const propTypes = Component.propTypes || {};

  return {
    bindings: toBindings(propTypes),
    controller: /*@ngInject*/ function($element) {
      this.$onChanges = () => {
        const props = toProps(propTypes, this);
        ReactDOM.render(<Component { ...props } />, $element[0]);
      };
      this.$onDestroy = () => ReactDOM.unmountComponentAtNode($element[0]);
    }
  };
}

export function getAngularService(name) {
  const injector = angular.element(document.body).injector();
  if (!injector || !injector.get) {
    throw new Error(`Couldn't find angular injector to get "${name}" service`);
  }

  const service = injector.get(name);
  if (!service) {
    throw new Error(`Couldn't find "${name}" angular service`);
  }

  return service;
}


// ---- todo-list.jsx ----

import React from 'react';
import { arrayOf, bool, func, shape, string } from 'prop-types';
import { getAngularService } from '<path>/angular-react-helper.jsx';

class TodoList extends React.Component {
  constructor(props) {
    super(props);
    // The way to get any AngularJS service from outside of the framework.
    this.$anchorScroll = getAngularService('$anchorScroll');
    this.goToTop = this.goToTop.bind(this);
  }

  goToTop() {
    this.$anchorScroll('title');
  }

  render() {
    return (
      <div>
        <h2 id='title'>{this.props.title}</h2>
        {this.props.items.map(item => (
          <label key={item.id} style={{ display: 'block' }}>
            <input
              type='checkbox'
              checked={item.isSelected}
              onChange={() => this.props.onSelect(item.id, !item.isSelected)}
            />
            {item.label}
          </label>
        ))}
        <a onClick={this.goToTop}>Go to top</a>
      </div>
    );
  }
}

// Must define all propTypes explicitly
// since they will be used to map angular inputs to react props.
TodoList.propTypes = {
  title: string,
  items: arrayOf(shape({
    id: string,
    label: string,
    isSelected: bool
  })),
  onSelect: func
};



// ---- todo-list-bridge.js ----

// This is all the code you need to create a bridge component.
import { reactToAngularComponent } from '<path>/angular-react-helper.jsx';
import TodoList from '<path>/todo-list.jsx';

angular
  .module('myModule')
  .component('todoListBridge', reactToAngularComponent(TodoList));



// ---- app.js ----

// An example of how to use the bridge component
// inside another Angular component.
class AppController {
  /*@ngInject*/
  constructor($scope) {
    this.$scope = $scope;
    this.todoItems = [
      { id: '1', isSelected: true, label: 'Wake up' },
      { id: '2', isSelected: false, label: 'Cook breakfast' },
      { id: '3', isSelected: false, label: 'Conquer the World' }
    ];
    // All inputs need to be passed as expression to bridge component,
    // so we bind "this" context to the controller method,
    // for the same reason we do it in React components.
    this.handleItemSelect = this.handleItemSelect.bind(this);
  }

  handleItemSelect(itemId, nextState) {
    // Controller properties passed to bridge component must be immutable,
    // otherwise its "$onChanges" life cycle method won't be called
    // and the underlying React component won't be updated.
    this.todoItems = this.todoItems.map(item => {
      if (item.id === itemId) {
        return Object.assign({}, item, { isSelected: nextState });
      }
      return item;
    });
    // Need to trigger digest cycle manually
    // since we changed todoItems from outside of the framework
    this.$scope.$apply();
  }
}

const appComponentConfig = {
  controller: AppController,
  // All inputs must be passed to the bridge component as expression.
  template: `
    <todo-list-bridge
      title="'My TODO List'"
      items="$ctrl.todoItems"
      on-select="::$ctrl.handleItemSelect"
    ></todo-list-bridge>
  `
};

angular.module('myModule').component('myApp', appComponentConfig);

At Awesense we follow simple rules to keep the migration going smoothly:

  • All new functionality is written in React;
  • If a developer touches old code, they rewrite it or part of it depending on the company’s business priorities at the time.

In the first year we switched 40% of our frontend code to React. After two years, more than two-thirds of our code base is now written in React.

I hope you feel more empowered knowing how AngularJS-React bridging works under the hood, and that the option to migrate to React does not look so daunting anymore.

Top comments (6)

Collapse
 
jade39 profile image
Jade

Nice One

Collapse
 
vineetgnair profile image
Vineet G Nair

Hello, this was a good article. I do have a doubt on how to handle the Angular JS component transclude and require option.

Collapse
 
chiptus profile image
Chaim Lev-Ari

I was wondering the same, did you solve it? Especially transclude

Collapse
 
vineetgnair profile image
Vineet G Nair

Yea, finally after a lot of try outs and failure.

  1. React does support transclude in a different way, check {props.children} concept in React.
  2. Require binding is a little complicated. I am not sure if I have the complete solution to this, but the point is, once your parent component has been rendered you can do angular.element(document.getElementsByTagName().data() or angular.element(document.getElementsByTagName().inheritedData() -- > from your child. You should see a object with all the necessary data (where will be your component name). The catch is, you must have your components rendered first.
Thread Thread
 
chiptus profile image
Chaim Lev-Ari

Thanks for the reply!

I know about the children. but if I pass to react an angular component, will it work?

Collapse
 
dannyk08 profile image
Danny Romero

Great great article!