React, Redux et Immutable.js : ingrédients pour des applications Web efficaces
Publié: 2022-03-11React, Redux et Immutable.js font actuellement partie des bibliothèques JavaScript les plus populaires et deviennent rapidement le premier choix des développeurs en matière de développement front-end. Dans les quelques projets React et Redux sur lesquels j'ai travaillé, j'ai réalisé que de nombreux développeurs débutant avec React ne comprenaient pas parfaitement React et comment écrire un code efficace pour utiliser tout son potentiel.
Dans ce didacticiel Immutable.js, nous allons créer une application simple à l'aide de React et Redux, et identifier certaines des utilisations abusives les plus courantes de React et les moyens de les éviter.
Problème de référence de données
React est une question de performances. Il a été conçu dès le départ pour être extrêmement performant, ne restituant que des parties minimales du DOM pour satisfaire les nouvelles modifications de données. Toute application React devrait principalement être constituée de petits composants simples (ou de fonction sans état). Ils sont simples à raisonner et la plupart d'entre eux peuvent avoir la fonction shouldComponentUpdate renvoyant false .
shouldComponentUpdate(nextProps, nextState) { return false; }
En termes de performances, la fonction de cycle de vie des composants la plus importante est shouldComponentUpdate et, si possible, elle doit toujours renvoyer false . Cela garantit que ce composant ne sera jamais restitué (à l'exception du rendu initial), ce qui rend l'application React extrêmement rapide.
Lorsque ce n'est pas le cas, notre objectif est de faire une vérification d'égalité bon marché des anciens accessoires/états par rapport aux nouveaux accessoires/états et d'ignorer le nouveau rendu si les données sont inchangées.
Revenons un instant en arrière et examinons comment JavaScript effectue des vérifications d'égalité pour différents types de données.
Le contrôle d'égalité pour les types de données primitifs comme boolean , string et integer est très simple puisqu'ils sont toujours comparés par leur valeur réelle :
1 === 1 'string' === 'string' true === true
D'autre part, la vérification d'égalité pour les types complexes tels que les objets , les tableaux et les fonctions est complètement différente. Deux objets sont identiques s'ils ont la même référence (pointant sur le même objet en mémoire).
const obj1 = { prop: 'someValue' }; const obj2 = { prop: 'someValue' }; console.log(obj1 === obj2); // false
Même si obj1 et obj2 semblent identiques, leur référence est différente. Puisqu'ils sont différents, les comparer naïvement dans la fonction shouldComponentUpdate entraînera un nouveau rendu inutile de notre composant.
La chose importante à noter est que les données provenant des réducteurs Redux, si elles ne sont pas configurées correctement, seront toujours servies avec une référence différente, ce qui entraînera un nouveau rendu du composant à chaque fois.
Il s'agit d'un problème central dans notre quête pour éviter le re-rendu des composants.
Gestion des références
Prenons un exemple dans lequel nous avons des objets profondément imbriqués et nous voulons le comparer à sa version précédente. Nous pourrions parcourir de manière récursive les accessoires d'objets imbriqués et comparer chacun d'eux, mais cela serait évidemment extrêmement coûteux et hors de question.
Cela ne nous laisse qu'une seule solution, et c'est de vérifier la référence, mais de nouveaux problèmes surgissent rapidement :
- Conserver la référence si rien n'a changé
- Modification de la référence si l'une des valeurs d'accessoires d'objet/tableau imbriqués a changé
Ce n'est pas une tâche facile si nous voulons le faire de manière agréable, propre et optimisée en termes de performances. Facebook s'est rendu compte de ce problème il y a longtemps et a appelé Immutable.js à la rescousse.
import { Map } from 'immutable'; // transform object into immutable map let obj1 = Map({ prop: 'someValue' }); const obj2 = obj1; console.log(obj1 === obj2); // true obj1 = obj1.set('prop', 'someValue'); // set same old value console.log(obj1 === obj2); // true | does not break reference because nothing has changed obj1 = obj1.set('prop', 'someNewValue'); // set new value console.log(obj1 === obj2); // false | breaks reference
Aucune des fonctions Immutable.js n'effectue de mutation directe sur les données données. Au lieu de cela, les données sont clonées en interne, mutées et s'il y a eu des modifications, une nouvelle référence est renvoyée. Sinon, il renvoie la référence initiale. La nouvelle référence doit être définie explicitement, comme obj1 = obj1.set(...);
.
Exemples React, Redux et Immutable.js
La meilleure façon de démontrer la puissance de ces bibliothèques est de créer une application simple. Et quoi de plus simple qu'une application todo ?
Par souci de concision, dans cet article, nous ne parcourrons que les parties de l'application qui sont essentielles à ces concepts. L'intégralité du code source du code de l'application est disponible sur GitHub.
Lorsque l'application est lancée, vous remarquerez que les appels à console.log sont commodément placés dans des zones clés pour montrer clairement la quantité de re-rendu DOM, qui est minime.
Comme toute autre application todo, nous souhaitons afficher une liste d'éléments todo. Lorsque l'utilisateur clique sur une tâche, nous la marquons comme terminée. Nous avons également besoin d'un petit champ de saisie en haut pour ajouter de nouvelles tâches et en bas de 3 filtres qui permettront à l'utilisateur de basculer entre :
- Tous
- Complété
- actif
Réducteur de Redux
Toutes les données de l'application Redux résident dans un seul objet de magasin et nous pouvons considérer les réducteurs comme un moyen pratique de diviser le magasin en plus petits morceaux plus faciles à raisonner. Étant donné que le réducteur est également une fonction, il peut également être divisé en parties encore plus petites.
Notre réducteur sera composé de 2 petites pièces :
- liste de choses à faire
- filtreactif
// reducers/todos.js import * as types from 'constants/ActionTypes'; // we can look at List/Map as immutable representation of JS Array/Object import { List, Map } from 'immutable'; import { combineReducers } from 'redux'; function todoList(state = List(), action) { // default state is empty List() switch (action.type) { case types.ADD_TODO: return state.push(Map({ // Every switch/case must always return either immutable id: action.id, // or primitive (like in activeFilter) state data text: action.text, // We let Immutable decide if data has changed or not isCompleted: false, })); // other cases... default: return state; } } function activeFilter(state = 'all', action) { switch (action.type) { case types.CHANGE_FILTER: return action.filter; // This is primitive data so there's no need to worry default: return state; } } // combineReducers combines reducers into a single object // it lets us create any number or combination of reducers to fit our case export default combineReducers({ activeFilter, todoList, });
Se connecter avec Redux
Maintenant que nous avons configuré un réducteur Redux avec les données Immutable.js, connectons-le au composant React pour transmettre les données.
// components/App.js import { connect } from 'react-redux'; // ….component code const mapStateToProps = state => ({ activeFilter: state.todos.activeFilter, todoList: state.todos.todoList, }); export default connect(mapStateToProps)(App);
Dans un monde parfait, la connexion ne doit être effectuée que sur les composants de route de niveau supérieur, en extrayant les données dans mapStateToProps et le reste est de base React transmettant les accessoires aux enfants. Sur les applications à grande échelle, il a tendance à être difficile de garder une trace de toutes les connexions, nous voulons donc les réduire au minimum.
Il est très important de noter que state.todos est un objet JavaScript régulier renvoyé par la fonction Redux combineReducers (todos étant le nom du réducteur), mais state.todos.todoList est une liste immuable et il est essentiel qu'il reste dans un tel formulaire jusqu'à ce qu'il réussisse la vérification shouldComponentUpdate .
Éviter le re-rendu des composants
Avant de creuser plus profondément, il est important de comprendre quel type de données doit être fourni au composant :
- Types primitifs de toute nature
- Objet/tableau uniquement sous forme immuable
Avoir ces types de données nous permet de comparer superficiellement les accessoires qui entrent dans les composants React.
L'exemple suivant montre comment différencier les accessoires de la manière la plus simple possible :
$ npm install react-pure-render
import shallowEqual from 'react-pure-render/shallowEqual'; shouldComponentUpdate(nextProps, nextState) { return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState); }
La fonctionshowEqual vérifiera les props /state diff uniquement sur 1 niveau de profondeur. Il fonctionne extrêmement rapidement et est en parfaite synergie avec nos données immuables. Avoir à écrire ce shouldComponentUpdate dans chaque composant serait très gênant, mais heureusement, il existe une solution simple.

Extrayez shouldComponentUpdate dans un composant séparé spécial :
// components/PureComponent.js import React from 'react'; import shallowEqual from 'react-pure-render/shallowEqual'; export default class PureComponent extends React.Component { shouldComponentUpdate(nextProps, nextState) { return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState); } }
Ensuite, étendez simplement tout composant dans lequel cette logique shouldComponentUpdate est souhaitée :
// components/Todo.js export default class Todo extends PureComponent { // Component code }
C'est un moyen très propre et efficace d'éviter le re-rendu des composants dans la plupart des cas, et plus tard, si l'application devient plus complexe et nécessite soudainement une solution personnalisée, elle peut être modifiée facilement.
Il y a un léger problème lors de l'utilisation de PureComponent lors du passage de fonctions en tant qu'accessoires. Étant donné que React, avec la classe ES6, ne lie pas automatiquement cela aux fonctions, nous devons le faire manuellement. Nous pouvons y parvenir en effectuant l'une des opérations suivantes :
- utilisez la liaison de fonction de flèche ES6 :
<Component onClick={() => this.handleClick()} />
- utiliser bind :
<Component onClick={this.handleClick.bind(this)} />
Les deux approches entraîneront un nouveau rendu de Component car une référence différente a été transmise à onClick à chaque fois.
Pour contourner ce problème, nous pouvons pré-lier les fonctions dans la méthode du constructeur comme suit :
constructor() { super(); this.handleClick = this.handleClick.bind(this); } // Then simply pass the function render() { return <Component onClick={this.handleClick} /> }
Si vous vous retrouvez à pré-lier plusieurs fonctions la plupart du temps, nous pouvons exporter et réutiliser une petite fonction d'assistance :
// utils/bind-functions.js export default function bindFunctions(functions) { functions.forEach(f => this[f] = this[f].bind(this)); } // some component constructor() { super(); bindFunctions.call(this, ['handleClick']); // Second argument is array of function names }
Si aucune des solutions ne fonctionne pour vous, vous pouvez toujours écrire les conditions shouldComponentUpdate manuellement.
Gestion des données immuables à l'intérieur d'un composant
Avec la configuration actuelle des données immuables, le re-rendu a été évité et nous nous retrouvons avec des données immuables à l'intérieur des accessoires d'un composant. Il existe plusieurs façons d'utiliser ces données immuables, mais l'erreur la plus courante consiste à convertir immédiatement les données en JS simple à l'aide de la fonction toJS immuable.
L'utilisation de toJS pour convertir en profondeur des données immuables en JS simple annule tout l'objectif d'éviter le re-rendu car, comme prévu, il est très lent et doit donc être évité. Alors, comment traitons-nous les données immuables ?
Il doit être utilisé tel quel, c'est pourquoi l'API Immutable fournit une grande variété de fonctions, mappe et est le plus couramment utilisé dans le composant React. La structure de données todoList provenant du Redux Reducer est un tableau d'objets sous forme immuable, chaque objet représentant un seul élément todo :
[{ id: 1, text: 'todo1', isCompleted: false, }, { id: 2, text: 'todo2', isCompleted: false, }]
L'API Immutable.js est très similaire au JavaScript normal, nous utiliserions donc todoList comme n'importe quel autre tableau d'objets. La fonction de carte s'avère la meilleure dans la plupart des cas.
À l'intérieur d'un rappel de carte, nous obtenons todo , qui est un objet toujours sous une forme immuable et nous pouvons le transmettre en toute sécurité dans le composant Todo .
// components/TodoList.js render() { return ( // …. {todoList.map(todo => { return ( <Todo key={todo.get('id')} todo={todo}/> ); })} // …. ); }
Si vous prévoyez d'effectuer plusieurs itérations enchaînées sur des données immuables telles que :
myMap.filter(somePred).sort(someComp)
… alors il est très important de le convertir d'abord en Seq en utilisant toSeq et après les itérations de le remettre sous la forme désirée comme :
myMap.toSeq().filter(somePred).sort(someComp).toOrderedMap()
Comme Immutable.js ne mute jamais directement des données données, il doit toujours en faire une autre copie, effectuer plusieurs itérations comme celle-ci peut être très coûteux. Seq est une séquence de données immuable paresseuse, ce qui signifie qu'il effectuera le moins d'opérations possible pour accomplir sa tâche tout en sautant la création de copies intermédiaires. Seq a été conçu pour être utilisé de cette manière.
Dans le composant Todo , utilisez get ou getIn pour obtenir les accessoires.
Assez simple non?
Eh bien, ce que j'ai réalisé, c'est que la plupart du temps, cela peut devenir très illisible avec un grand nombre de get()
et surtout getIn()
. J'ai donc décidé de trouver un compromis entre performances et lisibilité et après quelques expériences simples, j'ai découvert que les fonctions Immutable.js toObject et toArray fonctionnent très bien.
Ces fonctions convertissent de manière superficielle (1 niveau de profondeur) les objets/tableaux Immutable.js en objets/tableaux JavaScript simples. Si nous avons des données profondément imbriquées à l'intérieur, elles resteront sous une forme immuable prêtes à être transmises à
Il est plus lent que get()
juste par une marge négligeable, mais semble beaucoup plus propre :
// components/Todo.js render() { const { id, text, isCompleted } = this.props.todo.toObject(); // ….. }
Voyons tout en action
Si vous n'avez pas encore cloné le code de GitHub, c'est le moment idéal pour le faire :
git clone https://github.com/rogic89/ToDo-react-redux-immutable.git cd ToDo-react-redux-immutable
Le démarrage du serveur est aussi simple (assurez-vous que Node.js et NPM sont installés) que ceci :
npm install npm start
Accédez à http://localhost:3000 dans votre navigateur Web. Avec la console développeur ouverte, regardez les journaux lorsque vous ajoutez quelques éléments de tâches, marquez-les comme terminés et modifiez le filtre :
- Ajouter 5 éléments à faire
- Changez le filtre de 'Tous' à 'Actif' puis revenez à 'Tous'
- Pas de re-rendu todo, juste un changement de filtre
- Marquer 2 tâches comme terminées
- Deux todos ont été rendus à nouveau, mais un seul à la fois
- Changez le filtre de 'Tous' à 'Actif' puis revenez à 'Tous'
- Seuls 2 éléments de todo terminés ont été montés/démontés
- Les actifs n'ont pas été restitués
- Supprimer un seul élément de tâche du milieu de la liste
- Seul l'élément todo supprimé a été affecté, les autres n'ont pas été restitués
Emballer
La synergie de React, Redux et Immutable.js, lorsqu'elle est utilisée correctement, offre des solutions élégantes à de nombreux problèmes de performances souvent rencontrés dans les grandes applications Web.
Immutable.js nous permet de détecter les changements dans les objets/tableaux JavaScript sans recourir aux inefficacités des contrôles d'égalité approfondis, ce qui permet à React d'éviter des opérations de re-rendu coûteuses lorsqu'elles ne sont pas nécessaires. Cela signifie que les performances d'Immutable.js ont tendance à être bonnes dans la plupart des scénarios.
J'espère que vous avez aimé l'article et que vous le trouverez utile pour construire des solutions innovantes React dans vos futurs projets.