You put your React into my Angular

As our business grows, we are seeing more and more complex use-cases that put our UI under stress, such as hundreds of metrics and dimensions that make the rendering of the app in a localised place really challenging. We decided to tackle these problems in-depth, and came up with what we think is a balanced approach between React and AngularJS.

First, a word about AngularJS

We mostly use AngularJS as our JS framework of choice. We don’t want to enter the current flame war; we simply think it is great. We don’t buy the “it’s too complex” argument - we do software development so we have to understand the tools we use. The AngularJS abstraction and terminology are what they are, but once you wrap your head around that, they make a lot of sense. The depth of the framework is why it is called a framework, it solves a lot of common pain points in an elegant and efficient way. Mostly…

The only argument that we do agree with is that it can be fairly common to get UI performance issues in AngularJS. DOM manipulation is far from being optimal, and watchers trigger DOM rebuilds fairly often. To be fair we have upgraded from Angular 1.2 to 1.3 and it is getting better, but not good enough to get the native touch we are looking for on the most complex pieces.

Enter ReactJS

ReactJS is the all the rage now, and for good reason. A lot about it is great. Component orientation much more simple than AngularJS directives, uni-directional flow with flux (with the tradeoff of writing more code), and virtual DOM. This last one is a very cool innovation. For those not familiar with it: React manages a DOM in-memory, and when the state mutates and triggers any change to the DOM, mutations are first done in-memory. Then React computes the minimal set of mutations to do on the real DOM, and does them in batch. The result is pretty amazing, and the rendering speed - especially on complex components - is night and day compared to vanilla AngularJS.

So why not use ReactJS everywhere?

As a matter of fact, we think a lot of projects starting today will use React from top to bottom; we launched a sub-project and that is what we did. Nevertheless, the depth of the 2 frameworks are not comparable. You get much more from AngularJS than from React: Http services, gazillions of helper functions, 2-way data bindings, tons of Google and stack overflow answers, and much more. The result, in our experience, is that whether by design or simply the absence of an equivalent, you write more code using React than using Angular. My gut feeling - not scientific at all - is that our 120K lines of TypeScript / AngularJS code would turn into a 160K-line code base. Again, this is really more to illustrate my point than a proper fact based method and it does not take into account maintainability. It reminds me a bit of the Rails argument of the early years: with AngularJS you trade developer cycles for CPU cycles.

Toward the best of both worlds

What if you could have all the benefits of AngularJS with the precise touch of React in localised areas? Well you can, and here is our take on the subject.

The first thing we have to do is render the React component. You have 2 options for that, you can either use the ngReact directive or you can easily do it yourself by inserting an element in your template that will be used as the container: <div class="react-container"></div>, and then simply calling

React.render(
    React.createElement(MyElement, {}),
    element.find('.react-container').get(0)
);

The second problem we have to solve is how to get data from Angular to React and vice versa. To get data from Angular to React we create a module that will hold the data needed by the components. It looks like this:

module BimeQueryMeasuresStore {
    export var blender: OLAP.CubeModel.Blender;
    export var checkboxModelKey: string;
    export var measuresSearch: string;
 };

This is our way to implement the Flux stores. Those modules are accessible from everywhere in the application and, therefore, from the React views, so they can be used to populate the states of the React components.

To get data from React to Angular we have this helper function which allows us to trigger events on the $rootScope from React:

function $broadcast(event: string, ...args: any[]) {
    angular.element('body').injector().invoke([
        '$rootScope',
        '$timeout',
        ($rootScope: ng.IRootScopeService, $timeout: ng.ITimeoutService) =>
        {
            args.unshift(event);
            $timeout(() => {
                $rootScope.$broadcast.apply($rootScope, args);
            });
        }
    ]);
}

which can be used like this: $broadcast('update_data', {updatedFrom: 'React'})

This function allows us to send data from within the React components to Angular directives and so the one-way data flux is respected.

Use case

One of the main performance issues with Angular is rendering a really long list. This is usually done by ng-repeat, and this is what we were doing before we started using React. In order to illustrate this issue, please take a look at the gif below : data-structure-gif You can see that between the click on the cube icon and the display of the panel, 10 seconds have passed. That is because we have nearly 50 folders containing very complex elements. The total number of elements displayed approaches 500 so Angular struggles to render them. Please note this is an on purpose sick case.

Let’s get straight to the results. After implementing React, here is what we get : data-structure-react-gif For the same (too big) data, only 2 seconds have passed between the click and the display. That’s right, by using React, we have speeded up this part of the application by a factor of 5.

How did we do it?

If you have read the Thinking in react article, you will be familiar with the breaking of the UI and the state ownership problematic. If that’s not the case, let me sum it up.

Before jumping straight in to writing code, you need to identify which element is going to be a React component. Basically you travel down the visible hierarchy and each level is a component.

In our case it’s even more simple, every ng-repeat is going to be a React component containing a list of React components. It eventually gives the following hierarchy : Screen Shot 2015-03-11 at 14.16.44

The container is a list of folders, which are lists of attributes.

Once you have your hierarchy of elements, you need to identify which info you need in your states and which component will own the states.

This is a bit more tricky but the steps to follow are :

  • Identify every element that will need a set of information;
  • Find a common ancestor of all the previously identified elements;
  • This ancestor should own the state containing the needed information.

In our example it’s actually pretty simple. The container owns the state with the folders and the type of the folders. The other component owns a state with UI options such as the state (collapsed/expanded) of a folder.

Dive into the code

When we first started to implement React, the 0.13 version was already in beta so we decided to take advantage of one of its features, the compatibility with the ES6 class.

In order to be able to access every component from the others, we created a window variable like this : (<any> window).Views.CubeEditor = (<any> window).Views.CubeEditor || {};

All our classes are ‘stored’ in this variable.

Here is the definition of the container class :

(function() {
    class Container extends React.Component {
        constructor(props) {
            super(props);
            this.state ={
                folders: BimeCubeEditorFolderStore.folders,
                type: BimeCubeEditorFolderStore.type
            };
    }
    ...
    }
    Views.CubeEditor.Container = Container;
})();

Since we’re writing ES6 classes, we can set the initial state of the component directly in the constructor method instead of using the setInitialState method. As you can see, the initial state is initialized with the values from the store.

The last line before the end of the anonymous function ‘stores’ the class in the variable we defined previously.

Now let’s take a look at the mandatory render method :

render() {
    var folders = this.state.folders.map( (folder, index) => {
        var elements = folder.elements;
        return
        <Views.CubeEditor.Folder key={index} elements={elements}
        name={folder.displayName} type={this.state.type}/>
    });
    return <div className="folders">
        {folders}
    </div>
}

What we do here is turn every folder (in terms of Angular object) into a React folder component. Then, the return clause tells React what the DOM should look like when rendering a container. Here, it’s just a div with the class ‘folders’ containing all of the folders.

A word about the props

You might have noticed that the attributes values are wrapped in brackets. These attributes are called props. They are used to pass data from a component to its children.

They differ from the states because modifying them won’t trigger a render of the virtual DOM.

Let’s see how those props are used. Here is a part of the render method of the folders :

elements = this.props.elements.map((element, index) => {
    var uniqueId = _.uniqueId();
    return <Views.CubeEditor.Attribute dataId={index} key={uniqueId}
    attribute={element} folderName={this.props.name}/>;
});

Basically it does the same thing with the elements that we did with the folders. But we needed to be able to modify those elements without triggering a render of the DOM.

Note: we used _.uniqueId instead of index for the key because our folders implement a drag and drop functionality. The thing that makes React fast is that it maintains a virtual DOM and flushes the changes to the DOM only when it is needed and only where it is needed. The problem is that React tracks the components with the key attribute. But modifying the ordering of the keys won’t trigger a flush from the virtual DOM to the DOM. Using _.uniqueId guarantees us that the flush is made.

Illustration of Angular to React data flow

So far, we’ve just seen a basic React flow triggered once by Angular.

We are now going to see how a modification of an Angular $scope variable can and should make React do something.

You may have noticed the search input on top of the container. This is exactly what it looks like, a search field used to filter the elements. Because it doesn’t affect the performance, it is still written in Angular.

Here is how we handle it from the Angular directive :

$scope.$watch('searchElement', (newValue) => {
    BimeCubeEditorFolderStore.searchQuery = newValue;
    render();
});

It is a simple watch on the $scope variable. When the value changes, we update the value in the store and then ask React to re-render the panel. “Why didn’t you debounce your watch?” you might ask. We asked ourselves the same question and the answer is : React. Since React flushes modifications to the DOM only when and where they are needed, the impact of not debouncing the watch is really low.

Inside the render method of container we have this :

elements = folder.elements.filter((element) => {
    return _.contains(element.displayName.toLowerCase(),
    BimeCubeEditorFolderStore.searchQuery.toLowerCase());
}

Every time the search query changes, we filter the list of elements to be displayed.

React to Angular data flow

Since we chose to implement the flux stores with modules and the dispatcher with the $rootScope, if we want to keep the one-way data flow philosophy of React, we necessarily have to pass data from React to Angular. This is where the $broadcast helper method is used. Take a look at the code below :

elements.splice(to, 0, elements.splice(from, 1)[0]);
App.$broadcast(Events.FOLDER_DRAG_AND_DROP_END, elements, elements[0].folderName);

This is the code triggered at the end of a drag and drop. The first line just reorders the elements according to the drag and drop.

Now for the $broadcast method. Remember that the folders, and therefore the elements, are passed from an Angular $scope variable to a store, then to the state of container, then to the props of the folders. So if we want to actually make the change induced by the drag and drop, we have to update the store values and trigger the rendering.

Here is how we handle the event in the Angular directive :

$scope.$on(Events.FOLDER_DRAG_AND_DROP_END, (event, elements, folderName) => {
    $scope.folders.forEach( (folder: OLAP.CubeModel.Folder<any>) => {
        if (folder.displayName === folderName){
            folder.elements = elements;
        }
    });
    render();
});

We catch the event then find the right folder and update its elements. Finally, we trigger the Angular directive render method. Remember what this method does :

React.render(
    React.createElement(MyElement, {}),
    element.find('.react-container').get(0)
);

It recreates a container element that will get the values from the container and pass them through the props.

Summary of the data flow

The data are passed from Angular to the root React component via modules representing stores. Then the root component passes the data to its children through props. When a component needs to send data to Angular or even to one of its ancestors, it does so via a helper method sending an event to the $rootscope. By doing this, we respect the one-way data flow philosophy of React and therefore take advantage of the virtual DOM.

Conclusion

While it is nearly impossible to completely rewrite an application as big as ours, React can be used to improve performance in targetted places across the app. For now, we have just used it to improve the rendering of a big list but some other UI throttling can also be avoided by using React.

← Back to Home

Matthieu Ravey
comments powered by Disqus