From AngularJS to React Bit By Bit
Tags: react, angular
Published: 2020-02-26 06:37:35

> **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](#full-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](https://medium.com/@ajmeyghani/javascript-frameworks-performance-comparison-c566d19ab65b), easier to work with and has a larger community.



## Reasons

At [Awesense](https://www.awesense.com/) 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](https://github.com/ngReact/ngReact), [react2angular](https://github.com/coatue-oss/react2angular), [angular2react](https://github.com/coatue-oss/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](https://tech.small-improvements.com/how-to-migrate-an-angularjs-1-app-to-react/) and their open source [example](https://github.com/sfroestl/angular-react-migration).



## 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](https://docs.angularjs.org/guide/component) 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](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0) 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:

```jsx
import React from 'react';

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

An equivalent AngularJS component would look something like this:

```jsx
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:

```jsx
// ---- 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](https://docs.angularjs.org/guide/component) 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:

```jsx
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:

```jsx
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:


```jsx
// ---- 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`](https://reactjs.org/docs/react-dom.html#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](https://docs.angularjs.org/guide/component#component-based-application-architecture).

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

```jsx
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:

```jsx
// --- 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.

```jsx
// ---- 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](https://docs.angularjs.org/guide/expression#one-time-binding) syntax that tells AngularJS not to watch for `handleItemSelect` changes.



### Immutable Data

Let’s implement `handleItemSelect` logic.

```jsx
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](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/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](https://reactjs.org/docs/optimizing-performance.html#shouldcomponentupdate-in-action) 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()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_objects/Function/bind) method or define the method with a [fat arrow](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions) function as [class instance fields](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Class_fields), all of which will bind the right `this` under the hood.

```jsx
// 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.

```jsx
/*@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:

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

Add some sanity checks for easier debugging:

```jsx
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:

```jsx
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:

```jsx
// --- 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.

```jsx
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 <a name="full-example"></a>

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

```javascript
// ---- 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.