Flux & React: vers un nouveau paradigme

Flux & React: vers un nouveau paradigme

En se baladant sur le compte Github de Facebook, nous tombons rapidement sur des projets très intéressants : HHVM pour le côté PHP, Flux et React pour le côté Javascript. Nous allons nous arrêter sur ces derniers afin de comprendre les nouveaux paradigmes introduits par le géant bleu.

Pour rappel, React (à ne pas confondre avec reactphp ♥) est une librairie Javascript pour développer des interfaces utilisateurs. Cela fait maintenant quelques années que les framework MVC tels qu’Ember ou Angular font chacun partie intégrante des stacks web facilitant ainsi le développement d’applications web. Ceux-ci ont introduit des mécanismes de data-binding : le développeur ne s’occupe plus de synchroniser ses vues avec l’état des ses variables. Les données sont automatiquement mises à jour sur votre UI. Mais ceci a forcément un coût, en coulisse Angular effectue par exemple du dirty checking : à chaque cycle du navigateur, il compare une à une les variables bindées et met à jour les valeurs directement dans le DOM, opération très gourmande.

C’est là que React intervient.

Précisons que React n’est pas un concurrent à Angular mais plutôt un complément : la couche V du pattern MVC (même si Facebook introduit un nouveau pattern que nous verrons par la suite).

Comment marche React ? Contrairement aux autres frameworks, React dispose d’un DOM virtuel. Ainsi les opérations de mise à jour sont faites directement sur ce DOM chargé en mémoire et sont appliquées de manière différentielle puis appliquées sur le DOM physique. Résultat : un gain de performance notable. L’éditeur texte de Github Atom à d’ailleurs été réécrit avec React.

Création de notre premier composant

Partons donc à la découverte de React en codant quelque chose d’utile, à savoir un visualiseur de gifs utilisant l’API de Giphy, histoire de saboter 10 minutes de votre productivité une fois ce composant terminé. Pour commencer, quelques méthodes à connaître :

  • React.createClass(object) va permettre de créer un composant de votre UI ;
  • React.renderComponent(component, container) va permettre de générer le rendu de votre composant dans un container du DOM.

Un composant React doit obligatoirement avoir une méthode render() définissant son markup. Celui-ci peut-être écrit de deux manières : en JS ou en JSX. Le JSX est plus facile d’accès car il est très proche du HTML. Il faudra ajouter une étape de compilation JSX vers JS à votre workflow grâce à des outils comme Gulp ou Grunt selon vos préférences (ou Brunch, ou …).

Un composant React se définit par un state c’est à dire un état à instant t avec différentes propriétés. Ces propriétés ne doivent être modifiées qu’en interne. Pour initialiser l’état du composant, nous utilisons la méthode getInitialState({prop1: 'coucou'}). Pour modifier les propriétés de l’état, nous aurons recours à la méthode this.setState({prop1: 'poney'}). Nous pouvons passer des propriétés lors de l’instanciation du composant, celles-ci seront stockées dans l’objet this.props.

Voyons directement ces concepts avec le code de notre composant :

/**
 * @jsx React.DOM
 */

'use strict';

var Gifomatic = React.createClass({
    getInitialState: function() {
        return { gif: null };
    },
    getAwesomeGIF: function() {
        $.get('http://api.giphy.com/v1/gifs/tv?api_key=CW27AW0nlp5u0&tag=computer', function(response) {
            this.setState({gif : response.data.image_original_url});
        }.bind(this));
    },
    componentWillMount: function() {
        this.getAwesomeGIF();
        setInterval(this.getAwesomeGIF, this.props.delay);
    },
    render: function() {
        if (!this.state.gif) {
            return null;
        }
        return (
            <div className="viewer"><img src={this.state.gif} /></div>
        );
    }
});

La méthode componentWillMount() est lancée juste avant le rendu du composant. Dans notre cas on récupère le premier gif puis on récupère toutes les x secondes un nouveau gif (x étant définit par un paramètre donné au composant). Il existe plusieurs méthodes relatives au cycle de vie d’un composant.

Une fois votre composant de procrastination codé, il ne reste plus qu’à l’afficher grâce à la méthode renderComponent():

var Gifomatic = require('./js/components/gifotomatic');

React.renderComponent(
    <Gifomatic delay="3000"/>,
    document.getElementById('container')
);

Vous avez maintenant un Gifomatic© diffusant les meilleurs gifs du moment \o/

En plus de sa librairie React, Facebook apporte une nouvelle architecture dédiée en rupture avec le modèle MVC traditionnel.

Structurer notre composant avec Flux

Face au besoin continu d’ajout de nouvelles features, l’équipe frontend de Facebook s’est vite retrouvée bloquée à cause d’architectures Modèle-Vue-Contrôleur. Pour eux, le MVC n’est pas un pattern prévu pour scaler : la maintenabilité d’une application basée sur le design pattern MVC est laborieuse. Plus on ajoute de modèles, de contrôleurs et de vues, plus la complexité augmente.

Pour surmonter cette difficulté, Facebook a créé une « nouvelle » architecture appelée Flux. Contrairement au MVC, celle-ci est une architecture garantissant (en théorie) un flux unidirectionnel, permettant à un développeur d’identifier rapidement le chemin critique d’un événement et ses conséquences (vous trouverez ici un résumé du talk sur Flux lors du F8).

Pour cela, Flux s’appuie sur plusieurs concepts :

  • Les actions ;
  • Le dispatcher ;
  • Les stores ;
  • Les view (composants React).

Flux

Chaque interaction de l’utilisateur sur la view déclenche une action, celle-ci passe ensuite dans l’unique dispatcher qui notifie les stores. Le store prévient le composant qu’il a été modifié et celui-ci se met à jour.

Un store gère l’ensemble de données et la logique métier d’un domaine de l’application. Le dispatcher est quant à lui le seul point d’entrée des actions, ce qui permet de garder la main sur le code flow et de prévenir d’éventuels effets de bord.

Nous allons utiliser cette architecture afin d’ajouter de nouvelles killer-features à notre Gifomatic©.

Nous allons implémenter deux boutons pour naviguer entre les gifs. Commençons par créer l’unique dispatcher de notre application. Nous étendons celui fourni par flux en ajoutant une méthode handleViewAction() qui va permettre à notre vue de dispatcher des actions :

var Dispatcher = require('flux').Dispatcher;
var copyProperties = require('react/lib/copyProperties');

var AppDispatcher = copyProperties(new Dispatcher(), {
    handleViewAction: function(action) {
        this.dispatch({
            source: 'VIEW_ACTION',
            action: action
        });
    }

});

module.exports = AppDispatcher;

Notre composant comporte trois actions triviales :

  • Charger la liste des gifs ;
  • Afficher le gif suivant ;
  • Afficher le gif précédant.

Nous stockons ces actions comme des constantes afin d’y faire référence plus facilement (nous utilisons la lib keyMirror inclut dans React pour créer un objet avec des valeurs égales aux clés):

# GifomaticConstants.js

var keyMirror = require('react/lib/keyMirror');

module.exports = keyMirror({
    GIFOMATIC_LOAD_GIF: null,
    GIFOMATIC_NEXT_GIF: null,
    GIFOMATIC_PREVIOUS_GIF: null
});

Créons nos trois méthodes envoyant nos actions via le dispatcher précédemment créé :

# GifomaticActions.js

var AppDispatcher = require('../dispatcher/AppDispatcher');
var GifomaticConstants = require('../constants/GifomaticConstants');

var GifomaticActions = {
    loadGIF: function() {
        AppDispatcher.handleViewAction({
            actionType: GifomaticConstants.GIFOMATIC_LOAD_GIF
        });
    },

    nextGIF: function() {
        AppDispatcher.handleViewAction({
            actionType: GifomaticConstants.GIFOMATIC_NEXT_GIF
        });
    },

    previousGIF: function() {
        AppDispatcher.handleViewAction({
            actionType: GifomaticConstants.GIFOMATIC_PREVIOUS_GIF
        });
    }
};

module.exports = GifomaticActions;

et enfin notre store qui va contenir les données et le code métier :

# GifomaticStore.js

var AppDispatcher = require('../dispatcher/AppDispatcher');
var EventEmitter = require('events').EventEmitter;
var GifomaticConstants = require('../constants/GifomaticConstants');
var merge = require('react/lib/merge');
var $ = require('jquery');
var lodash = require('lodash');

var CHANGE_EVENT = 'change';

var _gifs = [];
var _current = null;

function loadGIF() {
    return $.get('http://api.giphy.com/v1/gifs/trending?api_key=CW27AW0nlp5u0', function(response) {
        _gifs = lodash.map(response.data, function(gif) { return gif.images.fixed_height.url; });
        _current = 0;
        GifomaticStore.emitChange();
    });
}

function nextGIF() {
    if (_current === (_gifs.length - 1)) {
        _current = 0;
    } else {
        _current++;
    }
}

function previousGIF() {
    if (_current === 0) {
        _current = (_gifs.length - 1);
    } else {
        _current--;
    }
}

var GifomaticStore = merge(EventEmitter.prototype, {

    getCurrentGif: function() {
        return _gifs[_current];
    },

    emitChange: function() {
        this.emit(CHANGE_EVENT);
    },

    addChangeListener: function(callback) {
        this.on(CHANGE_EVENT, callback);
    },

    removeChangeListener: function(callback) {
        this.removeListener(CHANGE_EVENT, callback);
    }
});

Toujours dans le même fichier, on enregistre nos callbacks dans notre dispatcher :

AppDispatcher.register(function(payload) {

    switch(payload.action.actionType) {
        case GifomaticConstants.GIFOMATIC_NEXT_GIF:
            nextGIF();
            break;

        case GifomaticConstants.GIFOMATIC_PREVIOUS_GIF:
            previousGIF();
            break;

        case GifomaticConstants.GIFOMATIC_LOAD_GIF:
            loadGIF();
            break;

        default:
            return true;
    }

    GifomaticStore.emitChange();

    return true;
});

module.exports = GifomaticStore;

Il ne nous reste plus qu’à modifier notre composant React pour qu’il écoute le store associé et se synchronise avec :

/**
 * @jsx React.DOM
 */

'use strict';

var React = require('react');
var GifomaticActions = require('../actions/GifomaticActions');
var GifomaticStore = require('../stores/GifomaticStore');

function getGifomaticState() {
    return {
        gif: GifomaticStore.getCurrentGif()
    };
}

var Gifomatic = React.createClass({
    getInitialState: function() {
        return getGifomaticState();
    },
    componentWillMount: function() {
        GifomaticStore.removeChangeListener(this._onChange);
    },
    componentDidMount: function() {
        GifomaticStore.addChangeListener(this._onChange);
        this._onInit();
        setInterval(this._onNextGif, this.props.delay);
    },
    render: function() {
        // Avoid render if gif isn't loaded
        if (!this.state.gif) {
            return null;
        }

        var divStyle = {
            backgroundImage: 'url(' + this.state.gif + ')'
        };

        return (
                <div className="viewer" style={divStyle}>
                    <button className="previousGIF" onClick={this._onPreviousGif}>{'<'}</button>
                    <button className="nextGIF" onClick={this._onNextGif}>{'>'}</button>
                </div>
        );
    },
    _onNextGif: function() {
        GifomaticActions.nextGIF();
    },
    _onPreviousGif: function() {
        GifomaticActions.previousGIF();
    },
    _onInit: function() {
        GifomaticActions.loadGIF();
    },
    _onChange: function() {
        this.setState(getGifomaticState());
    }
});

module.exports = Gifomatic;

Le Gifomatic est prêt ! Vous pouvez retrouver le code sur le dépot Github.

Bien entendu, l’architecture présentée est assez complexe pour un petit composant comme le Gifomatic, mais elle peut être très intéressante pour des projets de plus grosse envergure. On peut la voir en production sur Facebook, Github ou encore Instagram. Ce-dernier semble d’ailleurs s’appuyer considérablement sur React au vue de l’inspection du code source avec le React dev tools. React associé à l’architecture Flux peut donc s’avérer bénéfique pour vos applications Javascript de taille. La promesse est donc de gagner en scalabilité et de garantir à vos équipes de développement une meilleur compréhension de votre application. Notamment en simplifiant la phase de prise en main d’un projet disposant d’une base de code importante grâce à des flux uni-directionnels.

Devriez-vous utiliser React plutôt qu’Angular pour vos futurs projets ?

Le gain de performance apporté par le DOM virtuel de React est non négligeable. Avec Flux, Facebook apporte une vraie solution pour bâtir de solides applications. Quant à la courbe d’apprentissage, Angular semble plus complexe à appréhender. En effet, de nombreuses notions sont à assimiler : directives, services, filtres, contrôleurs, décorateurs. Dans son approche triviale, React semble plus facile d’accès: seule les notions de composants, d’états et de propriétés sont à découvrir. Cependant, la comparaison de React/Angular n’a de sens qu’en utilisant Flux. Ce-dernier est plus complexe à assimiler avec des notions peu courantes telles que le store où le dispatcher.

La structure intrinsèque de React/Flux promet cependant une simplification des processus de développement à long terme (voir ce mini retour d’expérience) contrairement à un projet Angular.

Ressources

blog comments powered by Disqus