Using ReactJS with AngularJS

-- By: @BorisDinkevich

tl;dr; How does ReactJS work in AngularJS? - A step-by-step tutorial.

The best way to follow the tutorial is by opening the initial codepen and adding/modifying stuff as you go along.

First codepan: http://codepen.io/borisd/pen/waVJZW

AngularJS on a page

Let's start with a super simple application. A single <pic> directive that renders a header:

angular.module('app', [])  
  .directive('pics',() => {
    return { 
      template: `<h2>AngularJS is here!</h2>`
    };
  });

And add the HTML we need to load it up:

<div id='app1'>  
  <pics></pics>
</div>  

Instead of using the usual ng-app="app" way, we will use the following (it will become clearer in a bit)

angular  
  .bootstrap(document.getElementById('app1'), ['app']);

We are basically getting a div on the page and mounting the app on it.

Updated code: http://codepen.io/borisd/pen/RPXVQG

Let's get crazy

In the codepen above you will notice we don't have one but rather three, divs with AngularJS running inside. Contrary to what some developers think, you can have AngularJS bound to a single container and run multiple apps on the same page!

Enter ReactJS

Let's create a simple ReactJS component (add it right above the AngularJS module declaration)

const Pic = React.createClass({  
  render() {
    return React.createElement('h2', {}, "ReactJS is here!");
  }
});

It should look familiar; it's just a ReactJS component that renders a header.

How do we load it? Just replace the bootstrap code for app3 with:

React.render(React.createElement(Pic), document.getElementById('app3'));  

Meditate on what happened for a minute.

ReactJS did the same as AngularJS and just bound itself to a container on the page.

Here is the updated code: http://codepen.io/borisd/pen/GJVWbJ

Embedding

So what does ReactJS need to load? Yep, a container. And if we want it to work inside AngularJS let us try adding a container into the directive... change the pic directive to:

angular.module('app', [])  
  .directive('pics', () => {
    return { 
      template: `
        <div>
          <h2>AngularJS is here!</h2>
          <div class="inside">inside</div>
        </div>`
    };
  });

And instead of loading ReactJS on #app lets load it inside of AngularJS:

React.render(  
  React.createElement(Pic), 
  document.getElementsByClassName('inside')[0]
);

Magic! - http://codepen.io/borisd/pen/NqQpZd

Remove the "jQuery"

We have ReactJS component loading inside of an AngularJS directive... but getElementsByClassName doesn't feel right. The proper way would be to have a nice directive take care of this for us.

What do we need inside it? A div for ReactJS to load on:

.directive('react', () => {
  return {
    template: '<div></div>'
  }
})

Now let's move the ReactJS render code into it, say inside the link function:

.directive('react', () => {
  return {
    template: '<div></div>',
    link: (scope, element, attrs) => {
      React.render(
        React.createElement(Pic), 
        document.getElementsByClassName('inside')[0]);
    }
  }
})

How do we access the div that we created in the template? Easy: element[0]. The final link function is then:

link: (scope, element, attrs) => {  
  React.render(React.createElement(Pic), element[0]);
}

Let's add the new directive into our AngularJS pic directive by changing the template to:

template: `  
  <div>
    <h2>AngularJS is here!</h2>
    <div class="inside" react>inside</div>
  </div> `

Notice the react directive usage in the inner div.

Much better! - http://codepen.io/borisd/pen/ZGgKzx

Can't have Pic running around

Our next problem will appear when we try to divide the project into files - we need to have Pic available to the react directive. We can always use import or require but AngularJS has a nicer way - "Dependency Injection".

We are going to wrap our ReactJS component into an AngularJS factory:

.factory('Pic', () => {
  return React.createClass({
    render() {
      return (React.createElement('h2', {}, "ReactJS is here!"));
    }
  }); 
})

Thats it! Just inject Pic into the react directive and the DI magic takes over.

The code - http://codepen.io/borisd/pen/yNmbNv

A directive per component?

Now we have both a factory for a component and a directive that renders it - too much code. Lets make our react directive be more versatile.

Instead of using it like:

<div class="inside" react></div>  

It will be much nicer to have:

<div class="inside" react="Pic">inside</div>  

Notice how we pass the Component name to the directive

Let's modify the react directive, so our current implementation looks like this:

link: (scope, element, attrs) => {  
  React.render(React.createElement(Pic), element[0])
}

To make it work we first need to figure out the name of the Component we want, and that already sits in the attrs we have in the link function: attrs.react (the second is the name of the directive).

Now we need to use Dependency Injection to get the actual Component. For that we will use the $injector service:

.directive('react', ($injector) => {
  return {
    template: '<div></div>',
    link: (scope, element, attrs) => {
      const component = $injector.get(attrs.react);
      React.render(React.createElement(component), element[0])
    }
  }
})

A nice clean reusable directive: http://codepen.io/borisd/pen/NqQjGj

What about data-binding?

Now that we have ReactJS fully "DI"ed and a nice way to use it in AngularJS let's talk about sharing data between the two.

The first method is quite straightforward, but first let's create some data to share by adding the following service:

.service('PicsService', function($interval) {
  this.data = { counter: 0 };

  $interval(
    () => this.data.counter += 1,
    Math.random() * 200 + 100
  );
})

Using arrow function in services will not work as angular needs to bind it to the object created by service()

The service adds a counter and increments it continuously (so we can see data-binding is fully functional).

To test it, inject it into our pic directive and add the following controller:

controller: ($scope) => {  
  $scope.data = PicsService.data;
},

Then we can see it working by just adding {{ data.counter }} inside the template.

What will we do for the ReactJS component? Let's try the same:

.factory('Pic', (PicsService) => {
  return React.createClass({
    render() {
      return React.createElement('h2', {}, 
        "ReactJS is here! ", 
        PicsService.data.counter);
    }
  }); 
})

Alas, we only got the initial value of the counter, there are no updates. To make ReactJS play in AngularJS's playground, we need to use the digest cycle. The simplest being $watch.

To use $watch we need a scope; a factory doesn't have one. So lets use the $rootScope.

One of the ways to use $watch is to have the first parameter be a function that the digest cycle will call every time, and compare its return value to the previous one. If they differ - that means we need to do an update.

In our case we will monitor the PicsService:

 $rootScope.$watch(
   () => PicsService.data.counter,
   (newVal) => this.setState({ counter: newVal })
 );

The full code for the factory:

.factory('Pic', ($rootScope, PicsService) => {
  return React.createClass({

    componentWillMount() {
      this.state = { counter: 0 };

      $rootScope.$watch(
        () => PicsService.data.counter,
        (newVal) => this.setState({ counter: newVal })
      );
    },

    render() {
      return React.createElement('h2', {}, 
        "ReactJS is here! ", 
        this.state.counter);
    }
  }); 
})

See it here: http://codepen.io/borisd/pen/aOeWNX

Made you shudder?

Good.

Delete all the crap, thats not how we want to use ReactJS. While this method is fine for updating the Service data:

$rootScope.$apply(() => PicsService.setCounter(100));

In ReactJS we prefer to pass props to Components.

Our pic directive's template should look like this:

<div class="inside" react="Pic" counter="data.counter">inside</div>

Notice how we pass the counter data with counter=""

This puts the burden of $watch on our re-usable react directive.

What we will need is to get the counter parameter out, which is easy using attrs.counter and then to get the value hidden behind data.counter

We will need to turn our

React.render(React.createElement(component), element[0])

into

React.render(React.createElement(component, { counter: ? }), element[0])

Luckly scope.$watch is to the rescue again:

const component = $injector.get(attrs.react);  
const render = (counter) => {  
  React.render(React.createElement(component, { counter }), element[0]);
} 

scope.$watch(  
  attrs.counter,
  render
);

First lets see that it's really working by changing the Pic componet to render:

render() {  
  return React.createElement('h2', {}, 
    "ReactJS is here! ", 
    this.props.counter);
}

Now it's this.props.counter instead of this.state.counter (as props come from outside).

The code: http://codepen.io/borisd/pen/YXmVVy

Explanation

How did that work?

First we moved the rendering into a function that receives the counter:

const render = (counter) => {  
  React.render(React.createElement(component, { counter }), element[0]);
}

Now calling render(100) will render the React component with this.props.counter set to 100.

Next we setup a watch on the directive's counter attribute which triggers a new render(newVal) everytime the value of the expression changes.

Magic.

Final notes

Now that you have seen how easy it is to add ReactJS to the project; use a library that is already well tested and used like ngReact. The purpose of this tutorial was to make you understand how things work behind the scenes and avoid pitfalls in the future.

While ReactJS and AngularJS play well together, allways consider why and if you should combine them. If speed is the thing you are after, read ReactJS performance. If you want to use ReactJS because its architecture fits best in some of the components on your page (e.g. Complex DatePicker), then you might be pleasently suprised.

Special thanks to Shai Davis for proofreading the post.

The sample code is also availble in the github repo

PS

Don't forget to clean your $watchers on desctruction - always.