React, la librairie JavaScript populaire pour la création d'interfaces utilisateur, offre aux développeurs un ensemble d'outils puissants pour créer des applications web interactives et dynamiques. L'un des principes clés du développement avec React est l'immutabilité, qui joue un rôle crucial dans l'assurance d'un rendu efficace, l'amélioration des performances et la gestion de l'état de l'application. Dans cet article, nous allons explorer le concept d'immutabilité dans React et en démontrer l'importance à travers des exemples de code pratique.
Comprendre l'immutabilité
L'immutabilité fait référence à la propriété d'un objet qui ne peut pas être modifié. Une fois qu'un objet immuable est créé, son état reste constant tout au long de sa durée de vie. L'immutabilité offre plusieurs avantages, notamment la simplicité, la fiabilité et une meilleure concurrence. Elle permet une programmation plus sûre et plus prévisible, car les objets ne peuvent pas être modifiés accidentellement, ce qui réduit le nombre de bugs liés à des changements d'état inattendus.
En JavaScript, les variables elles-mêmes ne sont pas intrinsèquement immuables ou mutables. Cependant, la manière dont vous affectez des valeurs aux variables et la nature de ces valeurs déterminent si elles sont mutables ou immuables.
Les variables qui contiennent des types de données primitifs tels que les nombres, les chaînes de caractères, les booléens, null et undefined sont immuables. Une fois affectées, leurs valeurs ne peuvent pas être modifiées. Si vous essayez de modifier une variable contenant une valeur primitive, cela crée en réalité une nouvelle valeur en mémoire.
let a = 5;
a++
console.log(a); // 6
Objets et listes : Les variables qui contiennent des objets (y compris des listes, des fonctions et des objets créés avec new Object()) sont mutables. Bien que la variable elle-même puisse être réaffectée à un autre objet, le contenu de l'objet peut être modifié.
// La méthode Array.push() est une méthode mutable car elle
// modifie directement la liste sans avoir besoin de le
// réaffecter à la variable. Son retour est la nouvelle longueur
// de la liste.
const a = [1, 2];
a.push(4); // [1, 2, 4]
// En JavaScript, il est possible d'injecter une nouvelle valeur
// à n'importe quelle position dans un tableau de la même manière
// que vous pouvez ajouter n'importe quel champ dans un objet.
const b = [1, 2, 3];
b[4] = 5; // [1, 2, 3, <1 empty slot>, 5]
Pour atteindre l'immutabilité avec des objets complexes tels que les objets et les listes, il suffit de s'assurer d'assigner une nouvelle instance des données à la variable. Par exemple :
// En utilisant l'opérateur de propagation (spread operator),
// nous pouvons copier le contenu de la variable c dans une
// nouvelle liste et y ajouter ce que nous souhaitons.
let c = [1, 2, 3];
c = [...c, 3];
// Les méthodes telles que slice renvoient une copie superficielle
// (shallow copy) de la liste. Avec cette nouvelle instance de la
// liste, nous pouvons utiliser des fonctions mutables comme
// push() pour manipuler le contenu de la liste sans affecter la
// variable d'origine.
const d = [1, 2, 3];
const updatedD = d.slice();
updatedD.push(4);
// concat() est un autre exemple d'une fonction de liste qui ne
// mute pas (ne modifie pas) la liste d'origine.
const e = [1, 2, 3];
const updatedE = e.concat(4);
Pourquoi est-il important d'utiliser l'immutabilité dans React ?
Pour comprendre pourquoi l'immutabilité est importante, nous devons comprendre le fonctionnement interne de React. Afin d'être efficace dans son rendu, React est composé d'un DOM virtuel (Document Object Model) qui est une représentation simplifiée et légère du DOM réel. React l'utilise pour mettre à jour et rendre les composants de manière efficace. C'est une couche d'abstraction entre la structure des composants et le DOM natif du navigateur.
Le cycle de vie d'un composant React se déroule en 4 étapes :
- Rendu du composant : Lorsqu'un composant React est rendu, il génère un arbre de DOM virtuel, qui est un objet JavaScript représentant la structure, les propriétés et les enfants du composant.
- Diffing : Lorsque l'état ou les props du composant changent, React crée un nouvel arbre de DOM virtuel pour le composant mis à jour et effectue un processus de "diffing", où il compare le nouvel arbre de DOM virtuel avec l'ancien pour identifier le plus petit ensemble de changements nécessaires pour mettre à jour le DOM réel.
- Réconciliation du DOM virtuel : React met à jour de manière efficace uniquement les parties du DOM réel qui ont changé, minimisant les rendus inutiles. Il calcule la différence entre les nouveaux et anciens arbres de DOM virtuel et n'applique que ces changements au DOM réel.
- Rendu : Enfin, React applique les changements calculés au DOM réel, ce qui se traduit par une interface utilisateur mise à jour.
La partie importante pour nous est la deuxième étape. La seule chose qui permet à un composant de se mettre à jour est un changement de props ou d'état. Prenons quelques exemples basiques de gestion d'état implémentés de différentes manières.
Mise à jour directe de la valeur d'état
const IncrementButton = () => {
let [value, setValue] = useState<number>(0);
return (
<button onClick={() => value = value + 1}>
{value}
</button>
);
};
Afin de mettre à jour l'interface utilisateur pour incrémenter la valeur, nous devons déclencher le cycle de vie de React. De cette manière, React est capable de mettre à jour le DOM virtuel et d'effectuer sa magie. Lorsque nous mettons à jour un état, il est nécessaire d'appeler l'action "setValue" fournie par le hook useState, car c'est la manière de React de déclencher le cycle de vie. Dans notre exemple, la mise à jour de la variable "value" ne déclenchera pas le cycle de vie et l'interface utilisateur ne sera pas mise à jour.
La nouvelle valeur ne sera même pas persistée lors des mises à jour ultérieures, car un composant React est une simple fonction JavaScript qui s'exécute à chaque mise à jour. Dans notre exemple, la déclaration de variable de "value" sera simplement recréée à partir de la valeur stockée dans useState. Par conséquent, il ne faut jamais réassigner directement la valeur d'un état, mais utiliser la méthode "setValue" à la place.
const IncrementButton = () => {
const [value, setValue] = useState<number>(0);
return (
<button onClick={() => setValue(value + 1)}>
{value}
</button>
);
};
Muter l'état avant de le mettre à jour
const NumberListing = () => {
const [numbers, setNumbers] = useState<number[]>([]);
const addNumberToList = () => {
numbers.push(numbers.length + 1);
setNumbers(numbers);
};
return (
<>
<button onClick={addNumberToList}>Add Number</button>
<span>Numbers:{numbers.join(', ')}</span>
</>
);
};
Dans cet exemple, nous avons un état contenant une liste de nombres que nous voulons étendre lorsque nous cliquons sur un bouton. Pour ce faire, le code utilise la fonction "push" de l'array pour y ajouter un élément. Comme mentionné précédemment, "push" est une fonction mutable de l'array. Étant donné que le type d'objet est un array, la constante "numbers" est simplement une référence de l'array défini dans l'état. Ces deux éléments signifient que l'utilisation de "push" sur la constante modifie la valeur interne de "useState". Alors, où est le problème ici ? Nous appelons bien la fonction setState après cela ! Pourquoi serait-ce problématique?
Le problème avec ce code vient de la gestion de l'état de React. Pour optimiser le rendu, React compare l'état actuel avec le nouvel état généré pour voir s'il y a effectivement un changement. En utilisant la fonction de mutation "push", nous mettons accidentellement à jour l'état avec la même valeur que la nouvelle, ce qui entraîne l'absence de différence entre les deux et empêche ainsi toute mise à jour inutile. L'immutabilité est primordiale afin d'informer React que les états vont être différents.
Dans cette situation, l'utilisation d'une solution immuable, comme l'opérateur de propagation (spread operator), résoudrait notre problème de rendu. Il en va de même pour les objets ou les classes JavaScript, car ils sont gérés par référence.
const NumberListing = () => {
const [numbers, setNumbers] = useState<number[]>([]);
const addNumberToList = () => {
setNumbers([...numbers, numbers.length + 1]);
};
return (
<>
<button onClick={addNumberToList}>Add Number</button>
<span>Numbers:{numbers.join(', ')}</span>
</>
);
};
Conclusion
Les problèmes de mutabilité dans React est une étape que tout développeur frontend va rencontrer lorsqu'il commence. Comme de nombreuses librairies et frameworks, comprendre le fonctionnement interne de React peut vous permettre de développer le meilleur produit de la manière la plus simple.
Lors de la mise à jour d'un état :
- Assurez-vous d'utiliser la fonction de mise à jour pour déclencher le cycle de vie de React.
- Assurez-vous que le nouvel état est différent de l'ancien. C'est simple avec les types primitifs, mais cela demande un peu plus d'imagination pour les types mutables, tels que les tableaux et les objets.
Les articles en vedette
Développement logiciel: comprendre ce que c'est
Les métaphores en développement logiciel
Cultiver le Growth Mindset : la clé de l’épanouissement en ingénierie logicielle
Soyez les premiers au courant des derniers articles publiés
Abonnez-vous à l’infolettre pour ne jamais rater une nouvelle publication de notre blogue et toutes nos nouvelles.