• 15 heures
  • Difficile

Ce cours est visible gratuitement en ligne.

course.header.alt.is_video

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 01/12/2023

Facilitez la manipulation du state avec Immer

Découvrez comment fonctionne useSelector

Dans le chapitre précédent, nous avons vu que lors d’un changement de state, useSelector  va utiliser la fonction de selector pour vérifier si le morceau de state sélectionné a changé ou non.

On peut donc se représenter la logique de useSelector  avec l'algorithme suivant :

// ⛔ ️ Attention ce n’est pas le vrai code de useSelector !
// Il s’agit seulement d’une représentation de la logique
 
// useSelector est une fonction qui prend un paramètre selector
function useSelector(selector) {
    // A chaque changement du state Redux...
    store.subscribe(() => {
    // ...on compare le résultat du selector
        if (selector(statePrecedent) === selector(stateActuel)) {
        // Si le résulat est le meme => pas besoin de mettre à jour
        } else {
        // si le résultat est différent => mise à jour du composant
            updateComponent();
        }
    });
}

La partie importante à noter dans ce code est que la comparaison est faite avec l’opérateur  ===   . Il est donc important de bien comprendre les égalités en JavaScript pour manipuler useSelector  comme un pro !

Revenez sur la manipulation de données en JavaScript

Observez le code ci-dessous et essayez de trouver ce qu’affiche la dernière ligne.

let lieuDeTravail2019 = 'bureau';
let lieuDeTravail2020 = lieuDeTravail2019;
lieuDeTravail2020 = 'maison';
// Qu'affiche la console ici ?
console.log(lieuDeTravail2019);

C’est bon, vous avez votre idée ? Vous ne trichez pas, j'espère 🧐 !

Alors vous avez trouvé ? Si vous avez devinébureau  , bravo ! C’est la bonne réponse. La modification de lieuDeTravail2020  n’affecte pas la variable lieuDeTravail2019  , elle conserve donc la valeurbureau  .

Observez maintenant ce deuxième extrait de code. Ici, on ne manipule plus des chaînes de caractères, mais des tableaux. Comme pour le premier cas, essayez de trouver ce qu’affiche la dernière ligne.

let materiel2020 = ['ordinateur'];
let matos2021 = materiel2020;
matos2021.push('webcam');
// Qu'affiche la console ici ?
console.log(materiel2020);

La réponse :  ["ordinateur", "webcam"]   !

C’est étrange 🧐, le code ne fait pourtant aucune modification sur materiel2020  ! Pourquoi le JavaScript ne se comporte pas de la même manière qu’avec des chaînes de caractères ?

Parce que les chaînes de caractères sont des valeurs primitives, alors que les tableaux utilisent des références.

Ah oui, j’ai déjà entendu ces termes je crois. Par contre, je n’ai jamais vraiment compris leur signification…

Ça tombe bien, j’ai une super technique pour vous l’expliquer ! Suivez-moi dans le screencast pour en savoir plus 👇 :

Vous comprenez maintenant pourquoi il est important d’utiliser le destructuring dans le reducer. Si on faisait une mutation de state, comme celle-ci par exemple :

if (action.type === "playPause") {
    // ⛔ ️ Le code ci-dessous est un contre-exemple !
    // Il ne faut surtout pas faire cela dans vos reducer !
    state.playing = !state.playing;
    return state;
}

La référence de state  reste la même, et React-Redux pourrait ne pas détecter le changement ! C’est pour cette raison qu’il faut utiliser le destructuring dans nos reducers et ne jamais changer directement le state !

Le destructuring est une notion essentielle en JavaScript mais dans la pratique, la syntaxe peut être un peu frustrante à utiliser, surtout lorsqu’il y a plusieurs niveaux. Heureusement pour nous, il existe une librairie JavaScript qui permet de faire changer un state sans faire de mutations : Immer !

Simplifiez vos Reducer avec Immer

Le destructuring, ce n’est pas simple, surtout lorsqu’il y a plusieurs niveaux, des tableaux, etc. Voici par exemple le même changement avec une mutation et avec du destructuring :

// sans le destructuring
users.admin.infos.age = 26;
 
// avec le destructuring
return {
    ...users,
    admin: {
        ...users.admin,
        infos: {
            ...users.admin.infos,
            age: 26
        }
    }
}

Je ne compte même plus le nombre de bugs que j’ai introduits à cause d’un mauvais destructuring ! Et puis il faut avouer que niveau lisibilité, ce n’est pas top…

Mais pas de panique, car depuis quelques années il existe un outil bien pratique qui va simplifier nos reducers : Immer !

Immer est une librairie JavaScript publiée en 2018, qui a un peu révolutionné la manipulation des données en JavaScript.

Avec Immer, plus besoin de destructuring, on fournit un state initial et des instructions de modifications, puis Immer va produire un nouveau state pour nous 🎉.

Des instructions de modifications ? Ça consiste en quoi exactement ?

Pour indiquer à Immer les changements que l’on souhaite faire sur notre state, nous allons faire les changements en question sur un draft (un brouillon). Ce brouillon nous sera fourni par Immer. Ce dernier va analyser les changements faits sur le draft pour produire le nouveau state.

Voici ce que cela donne avec du code :

// On importe la fonction produce d'Immer
import produce from "immer";
 
// Ici on déclare le state initial
const baseState = { playing: false };
 
// On appelle produce avec le state et une fonction
// cette fonction reçoit le draft en paramètre
const nextState = produce(baseState, (draft) => {
    // on peut modifier le draft directement !
    draft.playing = !draft.playing;
});
// Immer nous retourne un nouveau state
// Aucun changement n'a été fait sur le state initial !
console.log(nextState);

Ici on modifie le state sur un seul niveau, mais Immer permet de faire des changements sur plusieurs niveaux !

Par exemple, avec un state qui ressemble à ça :

const baseState = {
  todos: [
    { name: 'Apprendre React', done: true },
    { name: 'Maîtriser Redux', done: false },
  ]
}

On peut le modifier en utilisant Immer comme ça :

const nextState = produce(baseState, (draft) => {
  // On change le done de "Maîtriser Redux"
  draft.todos[1].done = true;
  // On ajoute un todo à la liste
  draft.todos.push({ name: 'Utiliser Immer', done: false });
});

Et pour la comparaison, les mêmes changements avec du destructuring ressemblent à ça :

const todosCopy = [...baseState.todos];
todosCopy[1] = {
    ...todosCopy[1],
    done: true,
}
const nextState = {
    ...baseState,
    todos: [
        ...todosCopy,
        { name: 'Utiliser Immer', done: false }
    ]
}

C’est tout de suite beaucoup moins facile à lire…

Finalement, Immer c’est un peu comme un collègue super fort en destructuring, on lui explique les modifications que l’on veut faire et il s'occupe de faire le destructuring à notre place !

Curieux de voir ce que cela donne de manière plus concrète ? Retrouvez-moi dans la vidéo ci-dessous pour mettre en place Immer dans le reducer de Tennis Score !

Vous pouvez retrouver le code de ce screencast sur la branche P2C2-immer du repository.

Exercez-vous

Vous avez tout compris ? Parfait, vous allez pouvoir le prouver dans cet exercice !

Vous devrez partir de la correction de l'exercice précédent (branche P2C1-solution) et mettre en place les deux changement suivants :

  • Améliorer la lisibilité du reducer grâce à Immer.

  • Ajouter un state pour sauvegarder un historique des jeux de tennis joués.

Astuce : Pour facilement suivre le state de Redux, vous pouvez ajouter le code suivant après la création du reducer (dans le fichier src/store.js  ) :

store.subscribe(() => {
    console.log("Nouveau state:");
    console.log(store.getState());
});

Une fois que vous avez terminé, allez jeter un œil à la version corrigée sur la branche P2C2-solution du repository.

En résumé

  • En JavaScript, il y a deux types de valeurs : les primitives (chaîne de caractères, nombre, boolean, null  et undefined  ) et les objets (objets et tableaux).

  • Il ne faut pas faire de mutation sur les objets de notre state, sinon cela change aussi le state précédent.

  • Pour éviter les mutations, on utilise le destructuring, parfois sur plusieurs niveaux.

  • Immer permet d’éviter la syntaxe de destructuring sans pour autant toucher au state précédent.

Dans le chapitre 1 de cette partie, nous avons vu l'usage de useSelector  pour lire une partie du state. Mais useSelector  permet de faire bien plus que cela. À la fin du prochain chapitre, les selectors n’auront plus de secret pour vous !

Exemple de certificat de réussite
Exemple de certificat de réussite