Publié par

Il y a 4 années -

Temps de lecture 8 minutes

Choisir une architecture Flux pour son projet React

React  est une librairie créée par Facebook permettant de générer des composants web au travers d’une API qui se veut simple et épurée. React est aujourd’hui utilisé en production par plusieurs entreprises telles que Facebook ou AirBnb avec de très bons résultats, et est donc bel est bien une alternative viable aux frameworks tels qu’Angular ou Ember.

Cependant l’utilisation de React, qui ne permet que de créer des composants, pose des questions quant à l’architecture logicielle qu’il faut utiliser : comment aller chercher sa donnée ? Comment mettre à jour les composants lorsque la donnée change ? Il est possible de choisir d’utiliser une approche MVC classique, avec Backbone ou encore Angular, mais l’approche semble peu naturelle et ne pas respecter la philosophie de React, qui encourage l’immutabilité et le flux de données à sens unique.

L’alternative au MVC proposée par Facebook se nomme Flux et cet article sera l’occasion d’analyser différentes options d’implémentation de Flux.

Flux "à la main"

Flux est une architecture (ça n’est pas une librairie, et encore moins un framework) simple et se caractérise par le fait que le flux de données est unidirectionnel.

Cette architecture contient des vues, les composants React, qui écoutent les modifications de store pour se mettre à jour. En cas d’évènement utilisateur (clique, frappe sur le clavier, etc.), le composant React appelle une méthode d’un objet Action qui lui même appelle le dispatcher, servant ici de "bus d’évènements". Les stores écoutent le dispatcher et se mettent alors à jour en fonction des évènements envoyés.

Facebook propose flux via npm. Celle-ci ne fait que mettre à disposition une classe de base pour le Dispatcher. Il reste donc au développeur une quantité non négligeable de boilerplate à écrire :

  • store intégrant un système d’émission d’évènements, généralement via l’eventEmmiter de node
  • actions s’appuyant sur des chaînes de caractères, impliquant généralement la création d’une classe de constantes
  • composants devant attacher l’écouteur lorsqu’il est monté et le détacher lorsqu’il est démonté.

Exemple :

//On utilise ici browserify, permettant d'utiliser le système de module commonjs
var React = require('react');
var _ = require('lodash');
var EventEmitter = require('events').EventEmitter;
var Dispatcher = require('flux').Dispatcher;
 
//Instance unique du dispatcher
var AppDispatcher = new Dispatcher();
 
//Constantes pour éviter de devoir réécrire des chaines de caractères
var NameConstant = {
    ADD_NAME: 'ADD_NAME',
    CHANGE_EVENT: 'change'
};
 
//Action
var NameAction = {
    addName: function (name) {
        AppDispatcher.dispatch({
            actionType: NameConstant.ADD_NAME,
            name: name
        });
    }
};
 
//Store héritant de l'eventEmitter
var NameStore = _.assign({
    names: ['Jurgen-Helmut', 'Georges']
}, EventEmitter.prototype);
 
//Ecoute du dispatcher par le store.
AppDispatcher.register(function (action) {
 //Le switch case n'est ici pas justifié
 // mais est représentatif du cas réel avec beaucoup d'actions différentes
    switch (action.actionType) {
        case NameConstant.ADD_NAME:
            NameStore.names.push(action.name);
            NameStore.emit(NameConstant.CHANGE_EVENT);
            break;
        default:
            break;
    }
});
 
//Composant graphique
var App = React.createClass({
    getInitialState: function () {
        return {
            names: NameStore.names,
            currentName: ''
        }
    },
    componentDidMount: function () {
        NameStore.on(NameConstant.CHANGE_EVENT, this._loadNames);
    },
    componentWillUnmount: function () {
        NameStore.removeListener(NameConstant.CHANGE_EVENT, this._loadNames);
    },
    _loadNames: function () {
        this.setState({names: NameStore.names});
    },
    _addName: function (e) {
        e.preventDefault();
        NameAction.addName(this.state.currentName);
        this.setState({currentName: ''});
    },
    _onChangeName: function (e) {
        this.setState({currentName: e.target.value});
    },
    render: function () {
        return (
            <div>
                <form  onSubmit={this._addName}>
                    <input type="text" placeholder="Nom" value={this.state.currentName} onChange={this._onChangeName}/>
                    <input type="submit" value="Enregistrer" />
                </form>
                <ul>
                {this.state.names.map((name, index) =>
                        <li key={index}>{name}</li>
                )}
                </ul>
            </div>
        );
    }
});
React.render(<App />, document.body);

L’avantage de cette approche est qu’elle laisse énormément de possibilité au développeur, qui n’est pas dépendant d’une librairie tierce qui pourrait ne plus être maintenue dans quelques mois.
Cependant, les développeurs sont feignants, et ils préfèrent malgré tout ne pas réinventer la roue à chaque fois. De nombreuses librairies pour aider à l’implémentation de l’architecture Flux sont donc apparues. On parlera ici d’une des plus connue : Fluxxor

Fluxxor

Fluxxor se présente comme un ensemble d’outils permettant de simplifier l’implémentation de l’architecture flux. Fluxxor apporte :

  • une classe Flux servant à manager stores et actions, et qui s’occupe de gérer le dispatcher
  • une classe Store héritant de l’eventEmmiter et fournissant en plus des helpers pour écouter des actions
  • du sucre syntaxique dans les actions pour simplifier le dispatch d’évènements
  • des mixins pour les composants React simplifiant la mise à jour des composants

Un morceau de code valant toujours mieux qu’un long discours, on pourra retrouver le même exemple que ci-dessus, mais cette fois-ci en utilisant Fluxxor :

var React = require('react');
var Fluxxor = require('fluxxor');

//Constantes
var NameConstant = {
    ADD_NAME: 'ADD_NAME'
};
 
//Store
var NameStore = Fluxxor.createStore({
    initialize: function () {
        this.names = ['Jurgen-Helmut', 'Georges'];
        this.bindActions(
            NameConstant.ADD_NAME, this.onAddName
        );
    },
    onAddName: function (name) {
        this.names.push(name);
  //L'événement 'change' est codé en dur dans Fluxxor
        this.emit("change");
    }
});
 
//Action
var actions = {
    addName: function (name) {
        this.dispatch(NameConstant.ADD_NAME, name);
    }
};
 
//Déclaration de flux
var stores = {
    NameStore: new NameStore()
};

var flux = new Fluxxor.Flux(stores, actions);
 
//Composant graphique
 
//FluxMixin fourni au composant (entre autre) la méthode getFlux
var FluxMixin = Fluxxor.FluxMixin(React);
 
//StoreWatchMixin permet au composant d'écouter l'évènement 'change' du Store
var StoreWatchMixin = Fluxxor.StoreWatchMixin;
var App = React.createClass({
    mixins: [FluxMixin, StoreWatchMixin("NameStore")],
    getInitialState: function () {
        return {currentName: ''};
    },
    getStateFromFlux: function () {
        return {names: this.getFlux().store("NameStore").names};
    },
    _onChangeName: function (e) {
        this.setState({currentName: e.target.value});
    },
    _addName: function (e) {
        e.preventDefault();
        this.getFlux().actions.addName(this.state.currentName);
        this.setState({currentName: ''});
    },
    render: function () {
        return (
            <div>
                <form  onSubmit={this._addName}>
                    <input type="text" placeholder="Nom" value={this.state.currentName} onChange={this._onChangeName}/>
                    <input type="submit" value="Enregistrer" />
                </form>
                <ul>
                {this.state.names.map((name, index) =>
                        <li key={index}>{name}</li>
                )}
                </ul>
            </div>
        );
    }
});

React.render(<App flux={flux}/>, document.body);

Fluxxor permet de simplifier l’utilisation de Flux, mais certains grincheux trouveront que l’architecture Flux en elle-même présente des défauts et que si le concept de Flux de données unidirectionnel est bon, on peut aller plus loin. Et c’est pourquoi Reflux est né.

Reflux

Reflux est une librairie permettant de mettre en place une architecture ressemblant à Flux tout en s’affranchissant de concepts vu comme des erreurs de Flux :

  • Le dispatcher unique est supprimé ; chaque action devient son propre dispatcher

  • Les actions sont maintenant écoutables, et les stores peuvent les écouter directement sans switch case autour de chaîne de caractères
  • Les stores peuvent s’écouter entre eux
  • Les dépendances entre store sont gérées par des agrégations de stores et non plus par la méthode waitFor

 On retrouvera le même exemple que précédemment, mais cette fois-ci en Reflux :

var React = require('react');
var Reflux = require('reflux');
 
//Action
var addName = Reflux.createAction();
 
//Store
var NameStore = Reflux.createStore({
    init: function () {
        this.names = ['Jurgen-Helmut', 'Georges'];
        this.listenTo(addName, this.addName);
    },
    addName: function (name) {
        this.names.push(name);
        this.trigger(this.names);
    }
});
 
//Composant graphique
var App = React.createClass({
    mixins: [Reflux.connect(NameStore, "names")],
    getInitialState: function () {
        return {currentName: '', names: NameStore.names};
    },
    _onChangeName: function (e) {
        this.setState({currentName: e.target.value});
    },
    _addName: function (e) {
        e.preventDefault();
        addName(this.state.currentName);
        this.setState({currentName: ''});
    },
    render: function () {
        return (
            <div>
                <form  onSubmit={this._addName}>
                    <input type="text" placeholder="Nom" value={this.state.currentName} onChange={this._onChangeName}/>
                    <input type="submit" value="Enregistrer" />
                </form>
                <ul>
                {this.state.names.map((name, index) =>
                        <li key={index}>{name}</li>
                )}
                </ul>
            </div>
        );
    }
});
React.render(<App />, document.body);

Conclusion

Parmi les différentes approches proposées ci-dessus, même si elles sont toutes viables, notre préférence va à Reflux. En effet, on peut le voir sur les exemples, Reflux permet réellement d’avoir beaucoup moins à coder, tout en s’assurant que tout ce qu’il se passe soit explicite, ce qui reste un des objectifs de l’utilisation de Flux. Cependant, le monde de Flux est un monde qui évolue à toute vitesse et il ne se passe pratiquement pas une semaine sans qu’une nouvelle implémentation de Flux sorte. On pourra en découvrir un certain nombre sur cette page dédiée : les outils complémentaires à React.

Facebook a profité de la React.js Conf pour annoncer Relay, leur propre implémentation de Flux basé sur GraphQL. La librairie n’étant pas encore open-sourcé à l’heure où ces lignes sont écrites, il est pour l’instant difficile de savoir si Relay enterrera la concurrence une fois pour toute, ou s’il ne sera qu’une implémentation de Flux en plus.

Publié par

Publié par Benoît Lemoine

Développeur et fier de l'être, Benoit s'intéresse de près à tout ce qui peut permettre de créer une application web, du HTML aux sources de données, en passant par le javascript et les framework haute productivité. twitter : @benoit_lemoine

Commentaire

3 réponses pour " Choisir une architecture Flux pour son projet React "

  1. Publié par , Il y a 4 années

    Hello

    Dans votre implémentation vous utilisez « Array.push » ce qui n’est pas vraiment dans la philosophie React :)

    J’avais discuté avec Christophe Chedeau (@Vjeux) au premier meetup React et il disait que Facebook utilisaient Flux depuis quelques temps deja, avant que React n’existe meme. Il a été surpris que la communauté adopte aussi vite et propose autant d’implémentations, d’autant qu’il sait que Flux n’est pas parfait.

    Moi je n’ai jamais trop accroché à Flux. C’est mieux que rien mais a la base React et Flux ont été conçus pour rendre le dev plus scalable sur une grosse equipe, une grosse appli… On remarque quand meme de grosses similarités entre Flux et CQRS/EventSourcing avec (actionBuilder/action et command/event), d’autant que CQRS a aussi été proposé en partie pour rendre le domaine plus maitrisable et séparer les responsabilités.

    Moi ce qui me choque le plus dans les implementations de Flux actuelles ce sont:

    – Le fait que chaque store a son propre etat. Pourquoi n’y aurait-il pas un état global immuable et qu’on ne rendrait pas tout l’etat toujours du root component? Plutot que de devoir mettre des listeners partout sur les stores dans les vues?

    – Le fait qu’il y ait des dépendances entre stores. En CQRS on comprends vite l’interet d’avoir des composants autonomes qui recoivent un flux d’events et mettent à jour leur état (ie une DB de reporting par ex, mais afficher quelque chose dans le DOM c’est un peu du reporting au final non?). Ces composants ne communiquent pas entre eux, ils n’ont pas besoin puisqu’ils disposent tous des memes evenements. A ceux de choisuir ceux qui les interessent. On pourrait faire la meme chose en React a savoir créer des « widgets » ou chaque widget a un composant React principal et un store. Ca ne serait alors pas grave de stocker plusieurs fois la meme info dans plusieurs widgets: chaque widget organise ses données comme il le souhaite)

    Le probleme d’utiliser un meme store partout sur l’application c’est aussi rendre le code compliqué à refactorer. A voir sur le sujet: http://www.udidahan.com/2009/06/07/the-fallacy-of-reuse/

    Dans ma startup j’ai créé un petit framework qui utilise ces concepts et pour le moment ca marche plutot pas mal.
    A voir: http://stackoverflow.com/questions/25791034/om-but-in-javascript

    Pour une petite illustration de l’interet d’avoir un état unique global, voir https://www.youtube.com/watch?v=5yHFTN-_mOo
    Quelques idées qui me viennent:
    – Undo/redo out of the box avec du simple state-sourcing
    – Etat facilement serialisable de l’application: possibilité de transmettre le json au serveur pour qu’un dev puisse faire le rendu de cet etat et voir ce qui coince en cas de bug du render
    – Possibilité de synchroniser facilement l’etat de 2 browsers (comme sur la video, ou mieux avec des websockets par ex)
    – Enregistrer des videos au « format JSON » …

  2. Publié par , Il y a 4 années

    Merci pour ce commentaire très enrichissant :)

    Pour le push, effectivement la mutabilité n’est pas trop la philosophie React/Flux, etc. C’était un choix pour ne pas alourdir un peu plus les exemples.

  3. Publié par , Il y a 4 années

    J’y vois toujours une architecture MVC mais implémenté en mode événementiel : le C étant le triplet [Action, Dispatcher, Store] et le mode d’interaction entre C et V se faisant en mode réactif.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Nous recrutons

Être un Xebian, c'est faire partie d'un groupe de passionnés ; C'est l'opportunité de travailler et de partager avec des pairs parmi les plus talentueux.