Angular + React

Suite à la lecture de cet article visant à concilier Angular et React, j'ai tenté le coup. J'ai réécris l'une des directives de Sleek à l'aide de React afin de constater comment fonctionne React dans un premier temps, et si le gain est vraiment significatif. Avant ça, ces derniers jours, j'avais déjà commencé à réécrire certaines directives très lourdes faisant de gros usages de mécaniques angular en me passant de celle ci, mais en utilisant du javascript vanille. Pour schématiser, j'ai remplacé les ng-click par des element.bind('click'). Rien que ça a suffit à augmenter promptement les perfs et j'entreprend désormais d'écrire mes composants angular complexes de la sorte. Est-ce-que React peut encore m'aider ?

Mise en place

J'ai d'abord un peu galéré avec gulp car j'ai déjà une tâche pour transpiler mes fichiers js es6. J'ai donc choisi de placer mes composants React dans un dossier à part sous forme de services angular sans utiliser ES6 et de les exclure de ma tâche es6. Il était ensuite plus simple de cibler ces fichiers là pour les transpiler avec gulp-react (à ne pas confondre avec gulp-jsx sous peine de passer 1/2h à ne pas comprendre pourquoi rien ne se passe -_-).

Pour une directive polarity qui sert à afficher une UI permettant de choisir la polarité d'un message, je me retrouve alors avec les fichiers suivants :

  /scripts/
          /components/
                     /polarity-directive.js
          /react/
                /polarity.js

Le premier fichier est donc une directive Angular qui va simplement rendre le composant React correspondant en mettant à jour le modèle angular dès que nécessaire.

On pourra alors intégrer simplement dans une vue parente :

<p>La polarité est : {{ polarity }}</p>  
<polarity  
    ng-model="polarity"></polarity>

Ce qui donnera ceci :

Angular

Du coté d'Angular, la directive reste donc très simple. Elle écoute les changement du ngModel et met à jour sa valeur quand le composant React lui dit de le faire :

'use strict';

angular.module('sleek.components.react')  
.directive('polarity', function (React, ReactPolarity) {

  return {
    restrict: 'E',
    template: '<div></div>',
    replace: true,
    require: 'ngModel',
    link: function (scope, element, attrs, ngModel) {
      scope.setValue = function (value) {
        scope.$apply(function () {
          ngModel.$setViewValue(value);
        });
      };
      scope.$watch(function () {
          return ngModel.$viewValue;
        }, function (value) {
        React.render(React.createElement(ReactPolarity, {
            value: value,
            setValue: scope.setValue
          }), element[0]);
      });
    }
  };
});

À noter que je n'utilise pas ici JSX pour des soucis de transpilation ES6 croisée, mais rien n'empeche de le faire. Donc la plupart du temps, tout ce que fera la directive coté Angular sera de spécifier le nom du composant React et de faire entrer une valeur et d'en faire sortir une autre.

React

C'est évidemment du côté de React qu'on va avoir le plus de travail. Voici à quoi ressemble mon composant :

'use strict';  
angular.module('sleek.react')  
.service('ReactPolarity', function (React) {
  var PolarityItem = React.createClass({
    handleClick: function (e) {
      this.props.onSetSelected(this.props.children);
    },
    render: function () {
      return (
        <li
          data-mood={this.props.children}
          className={'tg-mood ' + (this.props.selected ? 'tg-mood-selected' : '')}
          onClick={this.handleClick}>
          {this.props.children}
        </li>
      );
    }
  });
  var Polarity = React.createClass({
    getInitialState: function() {
      return {
        selected: this.props.selected,
        open: false
      };
    },
    handleSetSelected: function (selected) {
      if (!this.state.open) {
        return;
      }
      this.setState({
        selected: selected
      });
      this.props.setValue(selected);
    },
    handleOpen: function () {
      this.setState({
        open: !this.state.open
      });
    },
    render: function() {
      return (
        <ul
          className={'tg-moods ' + (this.state.open ? 'opened' : '')}
          onClick={this.handleOpen}>
          <PolarityItem
            selected={this.state.selected === 'negative'}
            onSetSelected={this.handleSetSelected}>negative</PolarityItem>
          <PolarityItem
            selected={this.state.selected === 'neutral'}
            onSetSelected={this.handleSetSelected}>neutral</PolarityItem>
          <PolarityItem
            selected={this.state.selected === 'positive'}
            onSetSelected={this.handleSetSelected}>positive</PolarityItem>
        </ul>
      );
    }
  });

  return Polarity;
});

Tout d'abord, vous pouvez voir que je l'ai emballé dans un service Angular. Ceci car dans le peu de doc que j'ai lu avant de me lancer là dedans, j'ai cru comprendre qu'on ne pouvait instancier un composant React qu'à partir d'une variable référençant sa spécification. Il fallait donc injecter cette variable sans recourir à une vilaine variable globale. Merci à Angular pour ça.

On sort ensuite totalement du monde Angular et on construit le composant à la sauce React. Et c'est là que j'ai un problème. Je trouve React finalement assez laid. Laid dans le sens où il n'apporte rien, en terme de construction et de lisibilité, par rapport à ce que je faisais avec YUI3 ou qu'on peut faire avec du javascript vierge. Ce que vous pensez être du HTML n'en est même pas, c'est du JSX. Vous allez donc innocemment placer un class="ma-classe" et vous manger des erreurs jusqu'à comprendre qu'en fait, il faut utiliser className="ma-classe". Pour moi ça revient exactement à la même chose que de remplacer :

var ul = document.createElement('ul');  
ul.className = 'tg-moods ' + (this.state.open ? 'opened' : '');  
ul.addEventListener('click', onClick);  
var li = document.createElement('li');  
li.innerHTML = 'negative';  
li.className = 'tg-mood ' + (this.props.selected ? 'tg-mood-selected' : '');  
li.addEventListener('click', onClick);  
ul.appendChild(li);  
// etc

En :

var div = document.create('div');  
ul.innerHTML =  
'<ul class="tg-moods ' + (this.state.open ? 'opened' : '') + '">' +  
'  <li class="tg-mood ' + (this.props.selected ? 'tg-mood-selected' : '') + "></li>' +  
'</ul>';  
var ul = div.firstChild;  
ul.addEventListener('click', onClick);  
// Etc

C'est d'ailleurs ce qu'on obtient une fois le JSX transpillé. Du coup, on perd l'un des plus gros avantage d'Angular : permettre d'écrire des templates en HTML que l'intégrateur peut toucher sans tout péter. Mais surtout séparer la vue et sa logique.

Conclusion

Au final, je reste très dubitatif quand à React. Je ne dis pas que c'est un mauvais framework, il a forcément des atouts que je n'ai pas les moyens techniques de mesurer, mais je préfère favoriser une app en Angular afin de profiter de toutes les qualités du framework plutôt que de perdre en lisibilité et en maintenance. Même si je comprends parfaitement que dans certains contextes, ce sont les perfs qui priment, je pense qu'en se débrouillant correctement, on peut arriver à avoir du Angular fluide. Réécrire les composants en se passant des directives Angular mais en tapant directement le DOM permet d'arriver au mêmes fins que ce que propose React tout en gardant la vue HTML et la séparation du code.

Hadrien

Hi, I'm a french Javascript Lead Developer, Web Architect from Toulouse, France. I've worked for 12 years for many projects with YUI, AngularJS, Aurelia.io and now React and React native.

Toulouse, France https://hadrien.eu