AngularJS

Implémenter un « annuler / rétablir » avec AngularJS et le Command Pattern

Edit 18/11/2014

Une démo de l’implém. présentée dans cet article est visible à l’adresse suivante : http://bobey.github.io/angular-undo-redo/

Introduction

Les applications web que nous sommes capables de développer rivalisent depuis quelques années déjà avec leurs équivalents desktop. Il n’y a qu’à voir pour s’en convaincre, la puissance des outils en ligne tels que Google Docs ou Google Spreadsheet qui, notamment depuis leur refonte avec le Material Design de chez Google, n’ont vraiment plus grand chose à envier aux applis natives.

Pour arriver à un tel niveau d’interactivité au sein du navigateur, il a fallut évidemment compter sur Ajax, les librairies JS type prototype ou jQuery, l’émergence d’HTML 5 et, plus récemment, des frameworks frontend MV* type Backbone, EmberJS ou Angular. Avec ces derniers, nous sommes en mesure de développer des applications toujours plus complexes et toujours plus user-friendly. Les composants d’UI réutilisables open-sourcés (directives Angular, Components Ember ou React, …) sont légion sur Github et nous permettent à nous, développeurs, de nous concentrer sur la partie métier de notre application.

Mais si il y a bien une fonctionnalité qu’on a toujours du mal à implémenter au sein de nos applications, c’est le Annuler / Rétablir (ou le Undo / Redo, Cancel / Restore, …).

Les libs undo / redo pour Angular

Pour AngularJS, il existe plusieurs directives et services permettant d’ajouter un annuler / rétablir en observant automatiquement les modifications effectuées sur un objet du scope. Je pense notamment à Angular-Chronicle et Angular History. Ce type d’approches est très pratique lorsque les interactions de l’utilisateur avec l’application se limitent à la MAJ des propriétés de quelques objets de tel ou tel scope. En revanche, elles trouvent vite leur limite dans une application plus complexe où le frontend dialogue par exemple avec le backend via une API REST pour ajouter au supprimer des éléments dans une base de données quelconque.

Le Command Pattern

Pour répondre à cette problématique de manière plus globale qu’en observant automatiquement les changements sur un objet du scope, une des approches les plus connues est l’utilisation d’un dérivé du Pattern Command.

Sur Wikipedia, voilà ce qu’on dit en introduction pour expliquer ce qu’est le command pattern :

In object-oriented programming, the command pattern is a behavioral design pattern in which an object is used to represent and encapsulate all the information needed to call a method at a later time. This information includes the method name, the object that owns the method and values for the method parameters.

Ce pattern est beaucoup utilisé pour implémenter les comportements d’UI (clic sur un bouton d’IHM) mais est aussi particulièrement bien adapté pour modéliser notre fameuse fonctionnalité undo / redo. Toujours pour citer Wikipédia :

If all user actions in a program are implemented as command objects, the program can keep a stack of the most recently executed commands. When the user wants to undo a command, the program simply pops the most recent command object and executes its undo() method.

Command Pattern et AngularJS

Nous allons voir comment architecturer notre application Angular autour du Command Pattern pour rendre possible l’annulation sur la création / suppression des éléments d’un listing d’utilisateurs.

Commençons par décrire le partial associé au listing des utilisateurs. Pour chaque utilisateur, on affiche simplement le username et un bouton de suppression à côté.
On place également un bouton d’ajout d’un nouvel utilisateur au listing, au dessus de ce dernier. On prévoit enfin deux boutons Annuler et Rétablir qui, dans une application réelle auraient plutôt leur place dans une directive spécifique, affichée de manière générique sur le layout global.

Ci-dessous, le code épuré associé :

// src/partials/users/list.html
<h1>Liste utilisateurs</h1>

<a href data-ng-click="listCtrl.undo()">Annuler</a> -
<a href data-ng-click="listCtrl.redo()">Rétablir</a> -
<a href data-ng-click="listCtrl.addUser()">Ajouter</a>

<ul>
    <li ng-repeat="user in listCtrl.users | orderBy:'name'">
        <a href ng-click="listCtrl.removeUser(user)">Supprimer</a> -
        <strong>{{ user.username }}</strong>
    </li>
</ul>

Associé à ce template, un controller Angular « classique » pourrait être implémenté de la manière suivante :

// src/js/controllers/user-list-controller.js
/** @ngInject */
function UserListController(users, User, UserService) {

    this.users = users;

    this.undo = function() {
        // TODO
    };

    this.redo = function() {
        // TODO
    };

    this.removeUser = function(user) {

        UserService.remove(user).then(function() {
            $state.reload();
        });
    };

    this.addUser = function() {

        // Dans un cas réel, ces données proviendraient sans doute de la soumission d'un formulaire quelconque...
        var user = new User({
            username: 'obalais',
            firstname: 'Olivier',
            lastname: 'Balais'
        });

        UserService.save(user).then(function() {
            $state.reload();
        });
    };
}

UserListController.resolve = {

    /** @ngInject */
    users: function(UserService) {
        return UserService.getUsers();
    }
};

angular
    .module('awesomeapp.controllers')
    .controller('UserListController', UserListController);

La logique de récupération / sauvegarde de nos utilisateurs est externalisée dans un service Angular UserService et notre controller ne fait qu’appeler ce service et mettre à jour son scope en fonction des réponses reçues. Le UserService en question peut échanger avec une API REST, via des Web sockets, stocker et retrouver les utilisateurs en mémoire ou dans le localStorage, les détails d’implémentation ne sont pas important…

Il est temps maintenant de modifier notre implémentation en s’inspirant du Command Pattern. Le principe théorique est relativement simple. Comme expliqué sur Wikipédia, il s’agit de représenter toutes les actions utilisateurs annulables sous forme de commandes implémentant la logique d’exécution et la logique d’annulation puis de conserver dans une stack la liste de ces commandes exécutées.

Voici donc une interface possible pour nos commandes :

function Command () {
}

Command.prototype = {
    execute: function() {
        // return promise
    },

    reverse: function() {
        // return promise
    }
};

Pour gérer l’empilage et dépilage de ces commandes, il nous faut définir un service UndoService.
Ce dernier expose trois méthodes executeCommand, undo et redo.
Une implémentation possible est détaillée ci-dessous :

/** @ngInject */
function UndoService ($q) {

    var undos = [],
        redos = [];

    /* jshint -W004 */
    var UndoService = {};

    UndoService.undo = function() {

        if (!undos.length) {

            var deferred = $q.defer();
            deferred.resolve('nothing to undo');

            return deferred.promise;
        }

        var command = undos.pop();

        return command.reverse().then(function(data) {
            redos.push(command);

            return data;
        });
    };

    UndoService.redo = function() {
        if (!redos.length) {
            var deferred = $q.defer();
            deferred.resolve();

            return deferred.promise;
        }

        var command = redos.pop();

        return command.execute().then(function(data) {
            undos.push(command);

            return data;
        });
    };

    UndoService.executeCommand = function(command) {
        return command.execute().then(function(data) {
            undos.push(command);

            return data;
        });
    };

    return UndoService;
}

angular.module('awesomeapp.services')
    .factory('UndoService', UndoService);

Parmi les quelques parti-pris, on note que l’exécution d’une commande réinitialise systématiquement la stack des redos. On constate également que suite à un undo, on vérifie que la promesse a bien été résolue avant d’ajouter la commande dans la stack des redos, et vice-versa.

Pour pouvoir refactorer notre UserListController, nous avons besoin d’implémenter deux commandes : UserCreateCommand et UserDeleteCommand.

Commençons par la commande UserCreateCommand :

// src/js/commands/user-create-command.js
/** @ngInject */
function UserCreateCommand (UserService) {

    /* jshint -W004 */
    function UserCreateCommand (user) {
        this.userToCreate = user;
        this.createdUser = null;
    }

    UserCreateCommand.prototype = {
        execute: function() {
            return UserService.save(this.user).then(function(createdUser) {

                this.createdUser = createdUser;
                return createdUser;
            }.bind(this));
        },

        reverse: function() {
            return UserService.remove(this.createdUser).then(function() {
                this.createdUser = null;
            }.bind(this));
        }
    };

    return UserCreateCommand;
}

angular
    .module('awesomeapp.commands')
    .factory('UserCreateCommand', UserCreateCommand);

Et maintenant la commande UserDeleteCommand :

// src/js/commands/user-delete-command.js
/** @ngInject */
function UserDeleteCommand (UserCreateCommand) {

    /* jshint -W004 */
    function UserDeleteCommand (user) {

        var userToCreate = Angular.copy(user);
        userToCreate.setNew(false);
        this.createCommand = new UserCreateCommand(userToCreate);
        this.createCommand.createdUser = user;
    }

    UserDeleteCommand.prototype = {
        execute: function() {
            return this.createCommand.reverse();
        },

        reverse: function() {
            return this.createCommand.execute();
        }
    };

    return UserDeleteCommand;
}

angular
    .module('awesomeapp.commands')
    .factory('UserDeleteCommand', UserDeleteCommand);

Comme vous pouvez le voir, l’implémentation de cette dernière repose sur la logique dores et déjà définie dans la commande UserCreateCommand, évitant ainsi de dupliquer inutilement du code.

Il ne nous reste alors plus qu’à modifier notre controller pour passer systématiquement par le UndoService :

// src/js/controllers/user-list-controller.js
/** @ngInject */
function UserListController(users, User, UserCreateCommand, UserDeleteCommand, UndoService) {

    this.users = users;

    this.undo = function() {
        UndoService.undo().then(function() {
            $state.reload();
        });
    };

    this.redo = function() {

        UndoService.redo().then(function() {
            $state.reload();
        });
    };

    this.removeUser = function(user) {

        var deleteCommand = new UserDeleteCommand(user);

        UndoService.executeCommand(deleteCommand).then(function() {
            $state.reload();
        });
    };

    this.addUser = function() {

        var user = new User({
            username: 'olivier.balais',
            firstname: 'Olivier',
            lastname: 'Balais'
        });

        var createCommand = new UserCreateCommand(user);

        UndoService.executeCommand(createCommand).then(function() {
            $state.reload();
        });
    };
}

UserListController.resolve = {

    /** @ngInject */
    users: function(UserService) {
        return UserService.getUsers();
    }
};

angular
    .module('awesomeapp.controllers')
    .controller('UserListController', UserListController);

Le mot de la fin

L’architecture de base est en place et nous avons vu deux commandes basiques pour créer et supprimer des utilisateurs.

Toute la difficulté désormais pour le développeur réside dans l’implémentation de ces différentes commandes notamment dans les cas plus complexes où la modification d’une entité a des impacts sur d’autres objets de l’application. Chaque commande doit contenir TOUTE la logique nécessaire à son exécution bien sûr, mais surtout à son reverse.

Vous pouvez retrouver le code de cet article sur ce dépôt Github.

ReactJS

React.js, Grunt, browserify et CDN

Lorsqu’on attaque le développement d’une application frontend en Javascript, plusieurs questions se posent immédiatement pour le développeur :

  • Quels frameworks et librairies (Angular, Ember, React et Flux, …)
  • Comment organiser les fichiers sources (organisation technique, organisation fonctionnelle, …)
  • Comment gérer les modules (AMD compliant, CommonJS compliant, …)
  • Quel outil de build (Grunt, Gulp, …)

Dans cet article, nous allons voir comment créer un HelloWorld avec ReactJs, en utilisant Grunt pour construire l’application à partir des sources et browserify pour gérer les dépendances entre nos modules front et libs externes.

Vous pouvez retrouver le code final et les différentes étapes dans ces quelques commits poussés sur Github.

Sur la documentation du site officiel de React, un HelloWorld ressemble typiquement à ça :

<!DOCTYPE html>
<html>
  <head>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.11.2/react.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.11.2/JSXTransformer.js"></script>
  </head>
  <body>
    <div id="content"></div>

    <script type="text/jsx">
      /** @jsx React.DOM */
      var hello = React.createClass({
        render: function() {
          return <div>Hello, {this.props.name}!</div>
        }
      });

      React.renderComponent(
        <hello name="World" />,
        document.getElementById('content')
      );
    </script>

  </body>
</html>

Notre composant React.js est implémenté en plein milieu de notre code HTML, le JSX est transformé en JS à la volée par JSXTransformer au sein du navigateur et le  tout, dans un seul fichier index.html. Pas terrible évidemment dans le cadre d’une « vrai » application.

Intégration de Grunt avec React

Facebook fournit un outil en ligne de commande très bien fait pour transformer côté serveur le JSX en JS.

npm install -g react-tools
jsx --watch src/ build/

Mais dès que l’application grossit, qu’on ajoute des assets, qu’on veut compiler des fichiers LESS ou SASS, minifier nos sources et autres joyeusetés, l’utilisation d’un outil de build tel que Grunt ou Gulp devient vite indispensable. Il y a beaucoup d’exemples d’intégration de React avec Gulp sur le net donc pour faire original (ou old school), nous allons utiliser Grunt.

npm install -g grunt-cli
npm install react --save-dev
npm install grunt --save-dev

# pour copier nos assets :
npm install grunt-contrib-copy --save-dev

Pour rappel, React utilise une extension au Javascript appelée JSX dont l’objectif est de faciliter l’implémentation d’un composent UI (~HTML) au sein de code javascript. Facebook a même proposé un draft de spécification de la syntaxe JSX :

JSX is a XML-like syntax extension to ECMAScript without any defined semantics. It’s NOT intended to be implemented by engines or browsers. It’s NOT a proposal to incorporate JSX into the ECMAScript spec itself. It’s intended to be used by various preprocessors (transpilers) to transform these tokens into standard ECMAScript.

Différentes approches sont possibles pour intégrer Grunt avec React. Il existe un plugin grunt-react très bien fait pour transpiler automatiquement les JSX en JS.

Ici, nous allons utiliser une autre approche en gérant nos dépendances entre modules avec Browserify et en intégrant la traduction des JSX via un transformer browserify.

Intégration de Browserify

Browserify est une lib de gestion de modules clients (browser) compatible CommonJS (« à la » nodejs).

Browserify lets you require(‘modules’) in the browser by bundling up all of your dependencies.

Evidemment, on retrouve un plugin grunt pour l’intégrer à notre build.

npm install grunt-browserify --save-dev
// Au sein de notre Gruntfile
grunt.loadNpmTasks('grunt-browserify');

Nous verrons la configuration proprement dite du plugin un peu plus loin.

Structuration du code

Maintenant que nous avons intégré browserify, nous pouvons structurer notre code source de manière un tout petit peu plus évolutive que dans le HelloWorld de base. Ci-dessous un exemple avec un dossier src/ et un dossier de build/. Les jsx (composants) sont regroupés dans src/jsx/ et l’index.html est à la racine du dossier src/. Cette structure n’a d’intérêt que pour montrer l’utilisation de browserify et de l’outil de build mais mériterait sans aucun doute d’être améliorée dans le cadre d’une application complexe avec plusieurs composants, différents domaines fonctionnels, etc. On trouve d’ailleurs quelques starter-kits intéressants sur Github pour commencer une application Flux / React.

structure

En reprenant le HelloWorld présenté au début de cet article et pour suivre la structure décrite ci-dessus, nous allons découpé les sources en trois fichiers :

  • src/jsx/hello.jsx qui contiendra l’implémentation du composant React hello
  • src/jsx/app.jsx qui contiendra la partie instanciation du composant
  • src/index.html qui contiendra le layout du HelloWorld

Le point d’entrée de notre application est app.jsx, au sein duquel nous chargeons nos dépendances (frameworks, lib, composants) avec Browserify (require(‘…’)).

ReactJs étant nativement compatible CommonJS, un require(‘react’) est suffisant :

// src/jsx/hello.jsx
var React = require('react');

var Hello = React.createClass({

  render: function() {
    return <div>Hello, { this.props.name }!</div>
  }
});

module.exports = Hello;

Pour rendre notre composant Hello importable depuis un autre fichier source, on utilise module.exports en fin de fichier. Il ne nous reste plus qu’à importer notre composant depuis app.jsx :

// src/jsx/app.jsx
var React = require('react'),
    Hello = require('./hello.jsx');

React.renderComponent(
  <Hello name="World" />,
  document.getElementById('content')
);

Notre layout index.html n’a alors plus qu’à importer app.js, bientôt buildé via grunt :

<!DOCTYPE html>
<html>
    <head>
        <title>Hello ReactJS</title>
    </head>
    <body>
        <div id="content"></div>
        <script src="js/app.js"></script>
    </body>
</html>

Branchement de l’ensemble avec notre Gruntfile

Pour brancher le tout, nous ajoutons à notre Gruntfile la configuration de browserify avec le transformer reactify et nous configurons également grunt-contrib-copy pour copier notre layout (et les autres assets éventuels) :

// Gruntfile
module.exports = function(grunt) {
    grunt.initConfig({
        browserify: {
            options: {
                debug: true,
                extensions: ['.jsx'],
                transform: ['reactify']
            },
            hello: {
                src: 'src/jsx/app.jsx',
                dest: 'public/js/app.js'
            }
        },
        copy: {
            all: {
                expand: true,
                cwd: 'src/',
                src: ['index.html'],
                dest: 'public/'
            }
        }
    });

    grunt.loadNpmTasks('grunt-browserify');
    grunt.loadNpmTasks('grunt-contrib-copy');

    grunt.registerTask('default', ['browserify', 'copy']);
};

Il ne nous reste plus qu’à tester en exécutant la commande :

grunt

Normalement, on obtient alors auto-magiquement un gros fichier JS qui contient React, notre composant Hello et notre micro-application démo.

Et si on ouvre notre fichier index.html dans un navigateur, on obtient un beau HelloWorld sur fond blanc…

Et pour profiter d’un CDN ?

Comme vous avez pu le constater ci-dessus, lors du build, ReactJS se retrouve dans notre gros fichier app.js. Il peut être intéressant d’extraire les vendors pour différentes raisons :

  • Le temps nécessaire au build à chaque modification (pour peu qu’on utilise un plugin watch avec Grunt) augmente proportionnellement avec la taille des fichiers à parser. Ainsi, parser tout ReactJS n’est pas très optimisé
  • Les vendors changent moins souvent et peuvent avantageusement profiter d’une mise en cache navigateur. Pas besoin de recharger tout ReactJS à chaque modification d’une ligne de code dans notre application

Pour cela, on génère souvent au moins deux fichiers JS lors du build, un pour les sources de l’application et l’autre pour les vendors qui changent peu souvent. Dans notre exemple, la seule lib utilisée est ReactJS. Pourquoi alors ne pas profiter carrément d’un chargement de React via un CDN ? Tout en conservant nos require(‘react’) au sein de notre code…

Pour arriver à cela, une astuce constiste à spécifier à browserify que React est une librairie externe :

browserify: {
    options: {
        // [...],
        external: ['react']
    },
    // [...],
}

On utilise en parallèle le package npm browserify-shim, qui sert d’ordinaire à rendre browserifiables des libs qui ne le sont pas nativement :

npm install browserify-shim

On déplace alors les transform browserify directement au sein du package.json en rajoutant browserify-shim et en lui spécifiant l’emplacement du module React au sein de window.

Notre package.json ressemble alors à ça :

// package.json
{
    "browserify": {
        "transform": [
            "reactify", "browserify-shim"
        ]
    },
    "browserify-shim": {
        "react": "global:React"
    },
    "dependencies": {
        "reactify": "^0.14.0",
        "react": "^0.11.2"
    },
    "devDependencies": {
        "grunt": "^0.4.5",
        "grunt-browserify": "^3.1.0",
        "grunt-contrib-copy": "^0.7.0",
        "reactify": "^0.14.0",
        "browserify": "^6.2.0",
        "browserify-shim": "^3.8.0"
    }
}

Et notre fichier Gruntfile modifié complet :

// Gruntfile
module.exports = function(grunt) {
    grunt.initConfig({
        browserify: {
            options: {
                debug: true,
                extensions: ['.jsx'],
                external: ['react']
            },
            hello: {
                src: 'src/jsx/app.jsx',
                dest: 'public/js/app.js'
            }
        },
        copy: {
            all: {
                expand: true,
                cwd: 'src/',
                src: ['index.html'],
                dest: 'public/'
            }
        }
    });

    grunt.loadNpmTasks('grunt-browserify');
    grunt.loadNpmTasks('grunt-contrib-copy');

    grunt.registerTask('default', ['browserify', 'copy']);
};

Il ne nous reste plus qu’à charger React au sein de notre layout, via un CDN, à l’ancienne :

<!DOCTYPE html>
<html>
    <head>
        <title>Hello ReactJS</title>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.11.2/react.js"></script>
    </head>
    <body>
        <div id="content"></div>
        <script src="js/app.js"></script>
    </body>
</html>

Pour aller plus loin

Le but de cet article était de montrer comment partir d’un HelloWorld typique ReactJS et l’étendre pour y intégrer une gestion des dépendances avec Browserify, Grunt comme outil de build et le tout en chargeant ReactJS via un CDN.

Pour aller plus loin, voici quelques axes d’amélioration en vrac :

  • Générer des sourcemaps pour nos fichiers JS / JSX
  • Minifier / uglifier notre application

Vous pouvez retrouver le code final et les différentes étapes dans ces quelques commits poussés sur Github.

ReactJS

Facebook ReactJS – Quelques ressources pour bien commencer

Le framework frontend du moment est sans conteste AngularJS. Le nombre d’articles et tutoriaux qui lui sont dédiés, les statistiques de recherche Google associées, les likes et contributeurs sur Github ou encore le nombre de questions sur Stackoverflow sont autant de métriques allant dans ce sens.

Jusqu’à aujourd’hui, les développeurs avaient souvent tendance à comparer les points forts et points faibles des frameworks frontend suivants avant de se lancer dans un projet :

Mais depuis plusieurs mois, un nouveau venu fait beaucoup parler de lui : React.js. Cette librairie a été open-sourcée par Facebook mi-2013 et est le fruit d’un travail commun entre les équipes de Facebook et d’Instagram.
Facebook présente React comme une librairie Javascript pour construire des interfaces utilisateur.

React : A Javascript library for building user interfaces

ReactJS présente quelques atouts extrêmement intéressants dont son Virtual DOM qui permet notamment :

  • Des performances de rendering bluffantes
  • L’Isomorphisme. Les composants React peuvent être rendus simplement côté serveur sans PhantomJS. SEO friendly, perfs au premier chargement…

D’autre part, son approche orientée composant force le développeur à architecturer son application en petits éléments autonomes et imbriquables dans des composants de plus haut niveau. Un peu à la manière des directives Angular, des components Ember ou des futurs Web Components (lire à ce sujet, React VS Web Components), sauf qu’avec React, tout est composant.

Au sein de cet article, j’ai tenté de regrouper les ressources les plus intéressantes que j’ai pu trouver sur le net et qui m’ont permis de comprendre un peu mieux le fonctionnement de React, en quoi son approche diffère des frameworks existants tels que Angular ou Ember, qui utilise cette librairie aujourd’hui et quel peut-être l’intérêt de l’intégrer au sein d’un projet existant ou d’un nouveau.

Si vous avez connaissance d’un article ou d’un tuto qui ne figure pas dans la liste ci-dessous, je suis preneur ;-)

ReactJS vs le reste du monde – React, c’est quoi, pourquoi ?

React – Qu’est-ce que c’est ?

Is React a Template Library? Is React Similar to Web Components? Are the Virtual DOM and Shadow DOM the Same? Autant de questions qu’on peut se poser quand on commence à s’intéresser à React et auxquelles l’auteur de cet article très complet tente de répondre :

http://www.funnyant.com/reactjs-what-is-it/

6 raisons d’aimer React

http://www.syncano.com/reactjs-reasons-why-part-1/

Angular vs React.js pour des applications web complexes

http://blog.liip.ch/archive/2014/09/16/angularjs-vs-reactjs-for-large-web-applications.html

Facebook React vs Web components

http://programmers.stackexchange.com/questions/225400/pros-and-cons-of-facebooks-react-vs-web-components-polymer

Tutos et exemples de code

Facebook’s React.js

La documentation sur le site officiel est très riche et très complète. Le quickstart notamment est un passage obligé pour comprendre les concepts clés de React :

http://facebook.github.io/react/docs/getting-started.html

Intégration de React à Angular

Un article très intéressant proposant une solution pour améliorer la performance de rendu de longues listes dans Angular en utilisant ReactJS :

http://www.mono-software.com/blog/post/Mono/242/Improving-AngularJS-long-list-rendering-performance-using-ReactJS/

Un autre exemple très détaillé traitant du même sujet :

http://www.williambrownstreet.net/blog/2014/04/faster-angularjs-rendering-angularjs-and-reactjs/

Starter Kit

Un starter kit très complet intégrant React à l’architecture Flux de Facebook :

https://github.com/kriasoft/react-starter-kit

Divers

Qu’est-ce qu’une isomorphic webapp et comment React facilite l’implémentation de ce type d’application :

http://bensmithett.github.io/going-isomorphic-with-react/

Components, plugins, librairies

La quantité de composants ou plugins React est actuellement très inférieure comparée au nombre de directives Angular open-sourcées par la communauté, mais il existe déjà un bel annuaire qui les recense et facilite leur recherche :

http://react-components.com/

En bonus, l’application est open-source et implémentée avec React.

React Router

L’un des projets autour de React les plus populaires :

https://github.com/rackt/react-router

OM

Un framework ClosureScript construit autour de React :

https://github.com/swannodette/om

React.js, qui l’utilise ?

On peut légitimement se demander si React est prêt à être utilisé ou si il manque aujourd’hui beaucoup trop de maturité. Les grands noms du Web ont en tous cas l’air de se pencher très sérieusement sur cette librairie.

Feedly

Github

Github a choisi React pour résoudre un certains nombre de soucis de performances rencontrés lors de l’implémentation de leur éditeur Atom :

http://blog.atom.io/2014/07/02/moving-atom-to-react.html

Adobe

Adobe intègre ReactJS au sein de son éditeur brackets notamment pour le rendu du file tree.

http://www.kevindangoor.com/2014/05/react-in-brackets/

Divers

http://wiredcraft.com/posts/2014/08/20/why-we-may-ditch-angularjs-for-react.html

Un tour d’horizon des projets utilisant officiellement React :

http://facebook.github.io/react/blog/2014/10/17/community-roundup-23.html

Architecture d’une application avec React : Flux, Reflux…

React se présente comme étant le V du pattern MVC. On peut facilement l’intégrer au sein de frameworks complets comme Angular ou Backbone mais Facebook a présenté la façon dont ils utilisent React au sein d’une application complexe en documentant l’architecture de leur projet appelée Flux.

http://facebook.github.io/flux/

Yahoo Flux

Yahoo a adopté Flux et maintient plusieurs projets implémentants les différents composants de l’architecture Flux.

Yahoo a également publié un dépôt Github avec un exemple d’application utilisant Flux et React.

https://github.com/yahoo/flux-examples

Reflux

Reflux est une alternative à l’architecture Flux tentant de simplifier son approche tout en conservant le concept de flow de données unidirectionnel :

The goal of the refluxjs project is to get this architecture easily up and running in your web application, both client-side or server-side.

Le projet sur github : https://github.com/spoike/refluxjs

http://spoike.ghost.io/deconstructing-reactjss-flux/

600px-JavaScript-logo

Array.forEach n’est pas toujours la meilleure solution !

EDIT : Je vous invite à lire les retours très pertinents et intéressants de @naholyr, @BAKfr et @KoonePoew suite auxquels j’ai modifié les exemples et une partie du contenu de cet article.

Ajoutée sur ECMAScript 5 (ES5) aux côtés de nombreuses autres fonctions tableaux, Array.prototype.forEach permet de parcourir des tableaux javascript de manière moins verbeuse (plus moderne ?) qu’avec la classique boucle for.

var array = [1,2,3,4,5];
for (var i = 0 ; i < array.length ; i++) {
  console.log(array[i]);
}

Le bout de code ci-dessus peut alors être écrit de la manière suivante :

var array = [1,2,3,4,5];
array.forEach(function(value) {
  console.log(value);
});

Il faut bien reconnaître que c’est quand même plus sexy ! Je passe volontairement sur les subtilités de l’utilisation d’un callback et sur le contexte de this dans ce dernier pour m’attarder plutôt sur les quelques exemples de code qui vont suivre.

Je privilégie depuis longtemps l’utilisation du forEach ES5 en lieu et place du classique for et j’encourage également à se passer le plus possible des pseudo forEach implémentés au sein des frameworks tels que AngularJs (angular.forEach permet par exemple un parcours à la fois de tableaux classiques et de propriétés d’objets ~ tableaux associatifs…) en utilisant au besoin un script tel que es5-shim si le support des vieux navigateurs est obligatoire.

Mais si on applique bêtement cette règle, on en vient parfois à utiliser forEach en dépit du bon sens…

function isElementVisible(element) {

    var isVisible = false;
    element.children.forEach(function(child) {
        if (child.visible) {
            isVisible = true;
            // Ici, on continue alors qu'on a déjà trouvé notre réponse...
        }
    });

    return isVisible;
}

var plop = //...
if (!isElementVisible(plop)) {
    // ...
}

Beaucoup de développeurs habitués à jQuery et à son jQuery.each() avant de développer en « vrai » Javascript pensent qu’il suffit de faire un return false dans le callback de l’appel à Array.prototype.forEach pour breaker le parcours. Or, il n’y a en fait aucun moyen de stopper ce parcours. Partant de ce postulat, le bout de code ci-dessus a perdu en lisibilité, en performance et donc en intérêt.

Comme pointé par @naholyr, @BAKfr et @KoonePoew dans les commentaires, c’est en fait Array.prototype.some qui répond parfaitement à notre besoin. Comme expliqué sur le site de Mozilla, la méthode some() teste si certains éléments du tableau passent le test implémenté par la fonction fournie.

Notre code peut alors être très simplement réécrit de cette manière :

function isElementVisible(element) {

    return element.children.some(function(child) {
        return child.visible;
    });
}

Notez que some s’arrête dès que le callback renvoie true, ce qui est exactement ce que l’on recherche.

Prenons maintenant l’exemple de la recherche d’un élément par son code :

function findElementByCode(elements, code) {

    var searchedElement = null;
    elements.forEach(function(element) {
        if (element.code === code) {
            searchedElement = element;
        }
    });

    return searchedElement;
}

Là encore, pas moyen de breaker au sein du forEach() ce qui peut poser des problèmes sérieux de performance.

Pour améliorer la performance de cette fonction, il est possible de la modifier pour utiliser l’alternative Array.prototype.some qui, elle, autorise bien le break dans le callback :

function findElementByCode(elements, code) {

    var searchedElement = null;
    elements.some(function(element) {
        if (element.code === code) {
            searchedElement = element;
            return true;
        }
    });

    return searchedElement;
}

Cette fois, les performances sont de retour mais on perd encore un peu plus en lisibilité. On a ajouté un return pour breaker (on ne retourne pas la valeur recherchée) et on détourne l’utilisation de la méthode de test some.

ECMAScript 6 ajoute une fonction find() sur le prototype de Array qui permettra de réécrire à terme cette recherche de la manière suivante :

function findElementByCode(elements, code) {

    return elements.find(function(element) {
        return element.code === code;
    });
}

Et si finalement, en attendant la sortie et l’adoption d’ES6, la bonne vieille boucle for n’était tout simplement pas LA solution :

function findElementByCode(elements, code) {

    for (var i = 0 ; i < elements.length ; i ++) {
        if (elements[i].code === code) {
            return elements[i];
        }
    }

    return null;
}

Certains pourraient être tentés de mettre en cache la propriété length dans cette fonction pour des raisons de performances :

function findElementByCode(elements, code) {

    var elementsLength = elements.length;
    for (var i = 0 ; i < elementsLength ; i ++) {
        if (elements[i].code === code) {
            return elements[i];
        }
    }

    return null;
}

Attention à ce type de micro-optimisation qui n’a plus de sens dans les navigateurs modernes où la mise en cache de length est faite automatiquement à l’exécution. Comme souvent, il y a certainement des gains de performances bien plus important ailleurs avant qu’il ne soit nécessaire de complexifier le code source pour ce type de pseudo-optimisation (Voici le résultat d’un test intéressant sur jsperf pour vous convaincre : javascript length cache vs no cache).

Finalement, si je devais proposer une bonne pratique ce serait la suivante :

  • Utiliser le forEach natif ES5 pour les parcours intégraux de tableaux
  • Rechercher « s’il existe une méthode avec la sémantique que l’on cherche » (cf commentaire de @BAKfr)
  • Utiliser for (var i ; ...) dans les autres cas