Il était une fois… Reactjs (Episode 2)

Dans le précédent article, nous avons introduit les notions de propriétés et d’état de ReactJs. Nous allons, ici, nous intéresser un peu plus aux interactions entre les différents composants d’une page (partage/échanges de données)
Petit rappel sur l’état et les propriétés
En résumé les propriétés sont des informations accessibles par un composant initialisées lors de la création. Elles peuvent influer sur l’affichage du composant. On pourrait voir ces propriétés comme la configuration du composant.
L’état représente les informations dont dépend l’affichage du composant (jusque là pas de différence), mais qui ne sont accessibles que par le composant lui même. En pratique le composant va modifier son état en fonction d’évènements (abonnements/callback javascript, intéraction utilisateurs…) qui lui sont extérieurs.
En général un composant n’a jamais besoin de modifier ses propriétés. Une fois que celles ci sont définies (à l’initialisation du composant), il peut s’en servir pour définir son état initial, mais toutes les mises à jours qui suivent se feront à partir de mises à jour de l’état et non des propriétés.Généralement, un composant n’a pas besoin de modifier ses propriétés, puisque les valeurs de celles-ci proviennent d’autres composants (généralement les composants parents).
Exemple : le calculateurs de point par mot
Dans cet exemple, nous allons créer un outil qui calcule le nombre de points que peut rapporter un mot donné au scrabble. Nous allons avoir 2 composants :
- un premier composant avec un input dans lequel on va écrire le mot : le mot fera partie de l’état du composant (chaque modification de l’input change l’état du composant)
- un second composant qui va afficher le nombre de points en fonction du mot (transmis par le premier composant) : le mot fera partie des propriétés du composant. Ce composant sera un composant fils du premier
Définition de la classe du premier composant :
var component = <div className="div-class">var Tree = React.createClass({ render : function () { return (<div> <input value={this.state.newWord} onChange={this.updateWord}/> <ScoreCalculator word={this.state.newWord}/> </div>); }, getInitialState : function() { return{ newWord : '' } }, updateWord : function (e) { this.setState({newWord : e.target.value}) } });
Définition de la classe du second composant :
var ScoreCalculator = React.createClass({ render : function () { var points = this.calculate(this.props.word.toUpperCase()); return (<div> {points} pts </div>); }, calculate : function(word) { var total = 0; for (var i = 0; i < word.length; i++) { var letter = word.charAt(i); if(scoreOf[letter]) total += scoreOf[letter]; else return -1; }; return total; } });
- le premier composant est celui qui contient l’input. C’est donc à lui de gérer son état (de définir l’état initial et de définir à quel moment il considère que son état a changé). En dehors de render, il a donc 2 fonctions : getInitialState et une qui va servir à modifier l’état (via setState), updateWord appelée à chaque modification de la zone de texte (onChange). En se mettant à jour, ce composant va mettre à jour la propriété word du composant fils.
- un second composant a pour seule responsabilité de recalculer un nombre de points en fonction de sa propriété word. Il a donc une fonction en plus de render, la fonction calculate.
Petite remarque : les propriétés des composants React représentant des évènements (onClick, onUpdate…), ont comme valeurs des fonctions. Ces fonctions prennent en paramètre un évènement standard javascript. Ce qui de manière générale permet d’accéder à l’élément html à l’origine de l’évènement, et dans notre cas, cet évènement nous permet d’accéder à la valeur de l’input à l’origine de l’évènement.
Comment fait-on lorsqu’on a une liste d’éléments à afficher ?
Un usecase assez courant lorsqu’on développe un application (ou un site) web ou mobile, c’est celui d’afficher sur une page un ensemble d’éléments (des posts/tweet, contacts/amis, …). Il serait dommage de finir ce post sans un exemple. =)
Selon la techno/plateforme écrire une liste peux être plus ou moins simple. Avec ReactJS, ça reste relativement facile.
Pour afficher une liste d’éléments, la technique la plus courante est l’utilisation de la fonction map (fonction standarde du prototype “array” en javascript) sur un tableau de données, et retourner un composant React par élément du tableau. La fonction render renvoie toujours un élément, mais parmi les descendants de celui-ci, on va retrouver un tableau de composants correspondant à notre liste d’éléments.
Exemple : une liste de lettres
Pour cet exemple on suppose qu’on a un tableau de lettres accessible dans tout le fichier (letters)
Définition de la classe “liste de lettres de l’alphabet” :
var LettersSet = React.createClass({ render : function () { return (<div> {letters.map(function(item) { return <Letter itemValue={item} />; })} </div>;); }, });
Définition de la classe du composant lettre :
var Letter = React.createClass({ render : function () { return (<div>{this.props.itemValue}</div>); } });
- Dans sa fonction de rendu, le premier composant va créer (en utilisant la fonction map) un composant React par élément du tableau de lettres.
- Le second composant a la l’élément à afficher (dans notre cas, une lettre) en tant que propriété, il n’a pas d’autre responsabilité que de l’afficher (et donc pas d’autre fonction)
Petite remarque : On peut même imaginer avoir une fonction de rendu passée en tant que propriété de notre classe liste qui serait utilisée pour gérer le rendu de chaque élément du tableau. Ce qui nous permettrait d’avoir un composant liste plus générique
Dans la lignée des choses qu’on fait couramment
Souvent lorsqu’on développe un écran avec une liste, on ne fait pas qu’afficher les éléments, on va aussi vouloir en sélectionner un, et si possible même afficher son détail. On va aussi avoir besoin d’avoir un style différent pour l’élément de la liste qui est sélectionné (grand classique : la vue liste/details).
Exemple : le calculateurs de point par lettre
On va reprendre notre liste de lettres, et on va ajouter le fait d’afficher un nombre de points associés à une lettre à chaque fois que celle-ci est sélectionnée. Pour implémenter cet exemple, on aura 4 composants :
- un composant de liste qui va afficher nos composants lettres (à priori assez proche de l’exemple précédent)
- le composant lettre qui va gérer l’affichage d’une lettre (qui devra aussi déclencher la sélection d’une lettre)
- un composant de Score (qui va afficher un nombre de points en fonction de la lettre sélectionnée)
- et le composant racine (qui sera parent des composants de Liste et Score)
Définition de la classe du composant liste :
var Letters = React.createClass({ render : function () { var handler = this.props.onLetterSelected; return (<div> {letters.map(function(item) { return <Letter itemValue={item} whenClick={handler} />; })} </div>); }, });
Définition de la classe du composant lettre :
var Letter = React.createClass({ render : function () { var classname = this.state.selected ? 'selected' : ''; return (<div className={classname} onClick={this.handleClick}>{this.props.itemValue}</div>); }, getInitialState : function () { return {selected : false }; }, handleClick : function () { this.props.whenClick(this.props.itemValue); this.setState({selected : true}); } });
Définition de la classe du composant d’affichage du nombre de points :
var Score = React.createClass({ render : function () { return (<div> la lettre {this.props.letter} vaut {scoreOf[this.props.letter]} pts </div>); }, });
Définition de la classe du composant racine :
var Tree = React.createClass({ render : function () { return (<div> <Letters onLetterSelected={this.selectLetter}/> <Score letter={this.state.letter}/> </div>); }, getInitialState : function() { return{ letter : '' } }, selectLetter : function (newLetter) { return this.setState({letter : newLetter}); } });
Le composant racine étant le seul à pouvoir communiquer à la fois avec la liste et le score, c’est lui qui porte la responsabilité de transmettre les messages de l’un vers l’autre.
L’idée ici est que ce composant transmet un handler de clic au composant de liste. qui le transmettra à son tour à chacun de ses composants fils (les lettres).
A chaque clic sur une lettre, le handler (dont la définition se trouve dans le composant racine) est déclenché ce qui permet de changer l’état de du composant Score.
Le fait que tous les évènements passent par le composant racine pose problème. Il a non seulement une responsabilité de contenant, mais il est aussi responsable de la communication de tous les composants qui sont en dessous de lui.
On est aussi obligés de rajouter du code dans chacun des composants afin de pouvoir gérer la communication entre eux.
On imagine bien que sur une page un peu plus complexe, beaucoup plus d’évènements sont échangés entre tous nos composants. De plus le composant racine ne devrait être celui qui gère les changements d’état entre se différents composants fils.
Petite remarque : Dans notre exemple nous n’avons pas géré le fait qu’une lettre passe de l’état “sélectionné” à “non-sélectionné” lorsqu’une autre lettre est sélectionnée. Cela aurait pour effet de rendre le code bien plus compliqué. ^^
D’une manière générale, comment je gère la communication entre mes différents composants React ?
Et bien, les créateurs de ReactJs proposent une solution pour palier à ce genre de problèmes :
Flux
Flux est un pattern (et non un framework ou une api), servant à gérer les échanges d’informations entre tous les composants React d’une même page.
Le but de flux est dispenser nos composants de la gestion des échanges qui se passent entre eux.
L’idée va être d’avoir (en plus de notre arbre de composants) une structure servant uniquement à gérer ces échanges. Nous allons appeler cette structure (roll credits) le flux. =)
La communication se limite à des échanges soit de l’arbre de composants vers le flux, soit dans le sens inverse.
Petite remarque : c’est un pattern (comme MVC ou MVVM), ce qui signifie qu’il est possible d’avoir différentes implémentations. Par exemple, il existe à ce jour des librairies comme Fluxxor ou Reflux qui sont des librairies Javascript aidant à la mise en place de cette architecture.
Un peu de vocabulaire
On retrouve 4 types d’éléments dans une architecture Flux :
- les Vues sont en fait nos composants React. Leur responsabilité va ici se limiter à gérer leur état et leur affichage.
- Les Stores jouent le rôle de sources de données. Ils déclenchent des évènement à chaque fois que des données doit être mise à jour. Les Vues/Composants s’y abonnent et changent d’état en fonction de ces évènements.
- Les Actions sont les moyens par lesquels les Vues/Composants déclenchent des mises à jour (les Vues ne déclenchent jamais directement de modification sur un store). Les actions peuvent être appelées par n’importe quel autre élément (autre qu’une vue) : par exemple, dans le cas de affichage d’une donnée à intervalles réguliers, l’action peut être appelée par un timer.
- Le Dispatcher sert d’intermédiaire entre les Actions et les Stores, lorsqu’une Action est appelée, le Dispatcher a la responsabilité de notifier tous les Stores qui y son abonnés.
Petite remarque : Il n’y a pas nécessairement d’interactions entre les Vues et le Dispatcher. Certaines implémentations de flux telles que Reflux, n’exposent même pas de manière explicite la notion de Dispatcher. Cela peut permettre d’avoir un code plus simple. (Dans le cas de Reflux les Stores son directement abonnés aux Actions qui agissent donc comme des Dispatcher)
Mais rien ne vaut un petit exemple…
Nous allons maintenant reprendre l’exemple précédent de liste/détails et le ré-implémenter en introduisant la notion de Flux.
Petite remarque : Nous allons nous concentrer sur la mise en place de Flux et non sur son implémentation. La seul chose à garder en tête pour la suite est que l’appel à l’action Action.select va déclencher l’évènement du Store “Store.whenItemSelected”.
Globalement on va retrouver les mêmes composants mais légèrement différents
Définition de la classe du composant racine :
var Tree = React.createClass({ render : function () { return (<div> <Letters /> <Score /> </div>); }, });
Définition de la classe du composant liste :
var Letters = React.createClass({ render : function () { return (<div> {letters.map(function(item) { return <Letter itemValue={item} />; })} </div>); }, });
Définition de la classe du composant lettre :
var Letter = React.createClass({ render : function () { var classname = this.state.selected ? 'selected' : ''; return (<div className={classname} onClick={this.handleClick}>{this.props.itemValue}</div>); }, getInitialState : function () { return {selected : false }; }, componentDidMount : function() { var comp = this; Store.whenItemSelected(function(letter) { comp.setState({selected : letter === comp.props.itemValue}); }) }, handleClick : function () { Action.select(this.props.itemValue); } });
Définition de la classe du composant d’affichage du nombre de points :
var Score = React.createClass({ render : function () { return (<div> la lettre {this.state.letter} vaut {scoreOf[this.state.letter]} pts </div>); }, getInitialState : function () { return {letter : '' }; }, componentDidMount : function() { var comp = this; Store.whenItemSelected(function(newletter) { comp.setState({letter : newletter}); }) }, });
Observons maintenant classe par classe l’apport de la mise en place de l’architecture flux
La classe Tree (La racine) :
Son code se limite uniquement à la méthode render. En effet sa seule responsabilité désormais est de contenir et afficher ses composants enfants. Les messages transitant d’un des descendants à un autre de doivent plus passer par lui. Plus besoin donc de passer un handler à ses descendants.
La classe Letters (la liste de lettres) :
de la même manière que la classe Tree, sa responsabilité se limite uniquement à afficher la liste de lettres. Son code est donc lui aussi réduit à la fonction render.
La classe Score :
Cette classe a maintenant un état : “la lettre qui vient d’être sélectionnée”. On va donc retrouver dans le code de la classe, l’intelligence de la gestion de son état. En résumé cela va se traduire par : définir un état initial dans un premier temps, et puis s’abonner aux évènement d’un store afin d’être notifié lorsqu’il sera potentiellement nécessaire de changer la valeur de l’état. On va donc retrouver, en plus de la fonction render, la fonction getInitialState qui va définir l’état initial, et qui ici renvoie une lettre vide. On a aussi la fonction componentDidMount, appelée une fois que le composant est ajouté au DOM et dans laquelle on va s’abonner à l’évènement du Store “Store.whenItemSelected” déclenché lorsqu’une qu’une lettre est sélectionnée.
La classe Letter :
Cette classe a elle aussi un état : “sélectionné” ou “non sélectionné”. On va donc y retrouver les fonctions getInitialState et componentDidMount (pour les mêmes raison que dans la classe Score). L’état initial défini dans la fonction getInitialState est l’état “non sélectionné”. Etant donné que le changement d’état arrive quand une lettre est sélectionnée, on va retrouver dans la fonction componentDidMount, un abonnement à l’évènement du store “Store.whenItemSelected”. La nouveauté ici va être le click sur le composant qui dois déclencher sa sélection et dé-sélectionner une autre lettre. Le <div> retourné par la fonction render a une valeur sur sa propriété onClick. Cette valeur est la fonction “handleClick” à l’intérieur de laquelle on fait appel à l’action Action.select. Cet appel va déclencher l’évènement Store.whenItemSelected chez tous les autres composants de la classe Letters, qui pourront alors se désélectionner.
En résumé
Le principe de flux est de réduire au maximum la complexité de chacun des composants. On voit que certains composants sont réduits à leur fonction de rendu (render).
Chaque composant est entièrement responsable de la gestion de son état et de l’intelligence qui va avec. Les composants ayant le plus de code sont ceux qui vont avoir à gérer leur état en fonction d’un store (ou autre) ou à déclencher des actions.
Les composants ont moins de dépendances entre eux. Le code servant à faire passer un message d’un composant à un autre (la tuyauterie) disparaît.
Dernière remarque : Ce billet de blog a été écrit dans le but de comprendre le concept de Flux, ces avantages, et les effets que cette architecture a sur le code des classes de composants. Je n’ai pas voulu focaliser sur les différentes implémentations (possibles ou existantes). Ce qu’il y a à savoir c’est que dans l’exemple ci-dessus, l’appel à l’action Action.select va déclencher (en passant par un Dispatcher ou non) l’évènement du Store “Store.whenItemSelected”. Pour info, chaque framework implémentant flux le fait plus ou moins différemment, la seule chose qui ne change pas reste que l’appee à une action, déclenche (directement ou indirectement) des évènements au niveau des Stores
Super article bravo et merci !
très cool comme expli, c’est gentils