De meilleurs réducteurs avec Immer
Publié: 2022-03-10En tant que développeur React, vous devez déjà être familiarisé avec le principe selon lequel l'état ne doit pas être muté directement. Vous vous demandez peut-être ce que cela signifie (la plupart d'entre nous avaient cette confusion quand nous avons commencé).
Ce tutoriel rendra justice à cela : vous comprendrez ce qu'est l'état immuable et sa nécessité. Vous apprendrez également à utiliser Immer pour travailler avec un état immuable et les avantages de son utilisation. Vous pouvez trouver le code dans cet article dans ce référentiel Github.
Immutabilité en JavaScript et pourquoi c'est important
Immer.js est une petite bibliothèque JavaScript écrite par Michel Weststrate dont la mission déclarée est de vous permettre "de travailler avec un état immuable de manière plus pratique".
Mais avant de plonger dans Immer, rappelons rapidement l'immuabilité en JavaScript et pourquoi c'est important dans une application React.
La dernière norme ECMAScript (alias JavaScript) définit neuf types de données intégrés. Parmi ces neuf types, six sont appelés valeurs/types primitive
. Ces six primitives sont undefined
, number
, string
, boolean
, bigint
et symbol
. Une simple vérification avec l'opérateur typeof
de JavaScript révélera les types de ces types de données.
console.log(typeof 5) // number console.log(typeof 'name') // string console.log(typeof (1 < 2)) // boolean console.log(typeof undefined) // undefined console.log(typeof Symbol('js')) // symbol console.log(typeof BigInt(900719925474)) // bigint
Une primitive
est une valeur qui n'est pas un objet et qui n'a pas de méthodes. Le plus important pour notre discussion actuelle est le fait que la valeur d'une primitive ne peut pas être modifiée une fois qu'elle est créée. Ainsi, les primitives sont dites immutable
.
Les trois types restants sont null
, object
et function
. Nous pouvons également vérifier leurs types en utilisant l'opérateur typeof
.
console.log(typeof null) // object console.log(typeof [0, 1]) // object console.log(typeof {name: 'name'}) // object const f = () => ({}) console.log(typeof f) // function
Ces types sont mutable
. Cela signifie que leurs valeurs peuvent être modifiées à tout moment après leur création.
Vous vous demandez peut-être pourquoi j'ai le tableau [0, 1]
là-haut. Eh bien, dans JavaScriptland, un tableau est simplement un type spécial d'objet. Au cas où vous vous poseriez également des questions sur null
et en quoi il est différent de undefined
. undefined
signifie simplement que nous n'avons pas défini de valeur pour une variable alors que null
est un cas particulier pour les objets. Si vous savez que quelque chose devrait être un objet mais que l'objet n'est pas là, vous renvoyez simplement null
.
Pour illustrer avec un exemple simple, essayez d'exécuter le code ci-dessous dans la console de votre navigateur.
console.log('aeiou'.match(/[x]/gi)) // null console.log('xyzabc'.match(/[x]/gi)) // [ 'x' ]
String.prototype.match
doit renvoyer un tableau, qui est un type d' object
. Lorsqu'il ne peut pas trouver un tel objet, il renvoie null
. Retourner undefined
n'aurait pas de sens ici non plus.
Assez avec ça. Revenons à la discussion sur l'immuabilité.
Selon les documents MDN :
"Tous les types sauf les objets définissent des valeurs immuables (c'est-à-dire des valeurs qui ne peuvent pas être modifiées)."
Cette instruction inclut des fonctions car il s'agit d'un type spécial d'objet JavaScript. Voir la définition de la fonction ici.
Jetons un coup d'œil à ce que signifient les types de données mutables et immuables dans la pratique. Essayez d'exécuter le code ci-dessous dans la console de votre navigateur.
let a = 5; let b = a console.log(`a: ${a}; b: ${b}`) // a: 5; b: 5 b = 7 console.log(`a: ${a}; b: ${b}`) // a: 5; b: 7
Nos résultats montrent que même si b
est "dérivé" de a
, changer la valeur de b
n'affecte pas la valeur de a
. Cela vient du fait que lorsque le moteur JavaScript exécute l'instruction b = a
, il crée un nouvel emplacement mémoire séparé, y place 5
et pointe b
à cet emplacement.
Qu'en est-il des objets ? Considérez le code ci-dessous.
let c = { name: 'some name'} let d = c; console.log(`c: ${JSON.stringify(c)}; d: ${JSON.stringify(d)}`) // {"name":"some name"}; d: {"name":"some name"} d.name = 'new name' console.log(`c: ${JSON.stringify(c)}; d: ${JSON.stringify(d)}`) // {"name":"new name"}; d: {"name":"new name"}
Nous pouvons voir que la modification de la propriété name via la variable d
la modifie également dans c
. Cela vient du fait que lorsque le moteur JavaScript exécute l'instruction, c = { name: 'some name
'
}
, le moteur JavaScript crée un espace en mémoire, place l'objet à l'intérieur et pointe c
dessus. Ensuite, lorsqu'il exécute l'instruction d = c
, le moteur JavaScript pointe simplement d
vers le même emplacement. Il ne crée pas de nouvel emplacement mémoire. Ainsi, toute modification des éléments de d
est implicitement une opération sur les éléments de c
. Sans trop d'efforts, nous pouvons voir pourquoi c'est un problème en devenir.
Imaginez que vous développiez une application React et que quelque part vous souhaitiez afficher le nom de l'utilisateur sous forme de some name
en lisant à partir de la variable c
. Mais ailleurs, vous aviez introduit un bogue dans votre code en manipulant l'objet d
. Cela entraînerait l'affichage du nom de l'utilisateur en tant que new name
. Si c
et d
étaient des primitives, nous n'aurions pas ce problème. Mais les primitives sont trop simples pour les types d'état qu'une application React typique doit maintenir.
Il s'agit des principales raisons pour lesquelles il est important de maintenir un état immuable dans votre application. Je vous encourage à consulter quelques autres considérations en lisant cette courte section du fichier README Immutable.js : le cas de l'immuabilité.
Ayant compris pourquoi nous avons besoin d'immuabilité dans une application React, regardons maintenant comment Immer s'attaque au problème avec sa fonction de produce
.
Immer's produce
Fonction
L'API principale d'Immer est très petite et la fonction principale avec laquelle vous travaillerez est la fonction produce
. produce
prend simplement un état initial et un rappel qui définit comment l'état doit être muté. Le rappel lui-même reçoit une copie brouillon (identique, mais toujours une copie) de l'état dans lequel il effectue toutes les mises à jour prévues. Enfin, il produce
un nouvel état immuable avec toutes les modifications appliquées.
Le modèle général pour ce type de mise à jour d'état est :
// produce signature produce(state, callback) => nextState
Voyons comment cela fonctionne en pratique.
import produce from 'immer' const initState = { pets: ['dog', 'cat'], packages: [ { name: 'react', installed: true }, { name: 'redux', installed: true }, ], } // to add a new package const newPackage = { name: 'immer', installed: false } const nextState = produce(initState, draft => { draft.packages.push(newPackage) })
Dans le code ci-dessus, nous passons simplement l'état de départ et un rappel qui spécifie comment nous voulons que les mutations se produisent. C'est aussi simple que ça. Nous n'avons pas besoin de toucher à aucune autre partie de l'État. Il laisse initState
intact et partage structurellement les parties de l'état que nous n'avons pas touchées entre l'état de départ et le nouvel état. Une de ces parties dans notre état est le tableau des pets
de compagnie. Le produce
d nextState
est un arbre d'état immuable qui contient les modifications que nous avons apportées ainsi que les parties que nous n'avons pas modifiées.

Armés de ces connaissances simples mais utiles, examinons comment les produce
peuvent nous aider à simplifier nos réducteurs React.
Réducteurs d'écriture avec Immer
Supposons que nous ayons l'objet d'état défini ci-dessous
const initState = { pets: ['dog', 'cat'], packages: [ { name: 'react', installed: true }, { name: 'redux', installed: true }, ], };
Et nous voulions ajouter un nouvel objet, et lors d'une étape ultérieure, définir sa clé installed
sur true
const newPackage = { name: 'immer', installed: false };
Si nous devions le faire de la manière habituelle avec la syntaxe d'étalement d'objet et de tableau JavaScripts, notre réducteur d'état pourrait ressembler à ci-dessous.
const updateReducer = (state = initState, action) => { switch (action.type) { case 'ADD_PACKAGE': return { ...state, packages: [...state.packages, action.package], }; case 'UPDATE_INSTALLED': return { ...state, packages: state.packages.map(pack => pack.name === action.name ? { ...pack, installed: action.installed } : pack ), }; default: return state; } };
Nous pouvons voir que cela est inutilement verbeux et sujet à des erreurs pour cet objet d'état relativement simple. Nous devons également toucher chaque partie de l'État, ce qui est inutile. Voyons comment nous pouvons simplifier cela avec Immer.
const updateReducerWithProduce = (state = initState, action) => produce(state, draft => { switch (action.type) { case 'ADD_PACKAGE': draft.packages.push(action.package); break; case 'UPDATE_INSTALLED': { const package = draft.packages.filter(p => p.name === action.name)[0]; if (package) package.installed = action.installed; break; } default: break; } });
Et avec quelques lignes de code, nous avons grandement simplifié notre réducteur. De plus, si nous tombons dans le cas par défaut, Immer renvoie simplement l'état de brouillon sans que nous ayons besoin de faire quoi que ce soit. Remarquez comment il y a moins de code passe-partout et l'élimination de la propagation d'état. Avec Immer, on ne s'occupe que de la partie de l'état qu'on veut mettre à jour. Si nous ne pouvons pas trouver un tel élément, comme dans l'action `UPDATE_INSTALLED`, nous passons simplement à autre chose sans rien toucher d'autre. La fonction « produire » se prête également au curry. Passer un rappel comme premier argument à `produire` est destiné à être utilisé pour le curry. La signature du "produit" au curry est //curried produce signature produce(callback) => (state) => nextState
Voyons comment nous pouvons mettre à jour notre état antérieur avec un produit au curry. Nos produits au curry ressembleraient à ceci : const curriedProduce = produce((draft, action) => { switch (action.type) { case 'ADD_PACKAGE': draft.packages.push(action.package); break; case 'SET_INSTALLED': { const package = draft.packages.filter(p => p.name === action.name)[0]; if (package) package.installed = action.installed; break; } default: break; } });
La fonction de produit au curry accepte une fonction comme premier argument et renvoie un produit au curry qui ne nécessite maintenant qu'un état à partir duquel produire l'état suivant. Le premier argument de la fonction est l'état brouillon (qui sera dérivé de l'état à passer lors de l'appel de ce produit au curry). Vient ensuite chaque nombre d'arguments que nous souhaitons passer à la fonction.
Il suffit maintenant pour utiliser cette fonction de passer dans l'état à partir duquel on veut produire l'état suivant et l'objet action ainsi.
// add a new package to the starting state const nextState = curriedProduce(initState, { type: 'ADD_PACKAGE', package: newPackage, }); // update an item in the recently produced state const nextState2 = curriedProduce(nextState, { type: 'SET_INSTALLED', name: 'immer', installed: true, });
Notez que dans une application React lors de l'utilisation du crochet useReducer
, nous n'avons pas besoin de transmettre explicitement l'état comme je l'ai fait ci-dessus car il s'en occupe.
Vous vous demandez peut-être si Immer deviendrait un hook
, comme tout dans React ces jours-ci ? Eh bien, vous êtes en compagnie de bonnes nouvelles. Immer a deux crochets pour travailler avec l'état : les useImmer
et useImmerReducer
. Voyons comment ils fonctionnent.
Utilisation des crochets useImmer
et useImmerReducer
La meilleure description du crochet useImmer
provient du README use-immer lui-même.
useImmer(initialState)
est très similaire àuseState
. La fonction renvoie un tuple, la première valeur du tuple est l'état actuel, la seconde est la fonction de mise à jour, qui accepte une fonction de producteur immer, dans laquelle ledraft
peut être muté librement, jusqu'à ce que le producteur se termine et que les modifications soient apportées immuable et devenir l'état suivant.
Pour utiliser ces crochets, vous devez les installer séparément, en plus de la bibliothèque principale Immer.
yarn add immer use-immer
En termes de code, le crochet useImmer
ressemble à ci-dessous
import React from "react"; import { useImmer } from "use-immer"; const initState = {} const [ data, updateData ] = useImmer(initState)
Et c'est aussi simple que ça. On pourrait dire que c'est useState de React mais avec un peu de stéroïde. Utiliser la fonction de mise à jour est très simple. Il reçoit l'état de brouillon et vous pouvez le modifier autant que vous le souhaitez comme ci-dessous.
// make changes to data updateData(draft => { // modify the draft as much as you want. })
Le créateur d'Immer a fourni un exemple de codesandbox avec lequel vous pouvez jouer pour voir comment cela fonctionne.
useImmerReducer
est tout aussi simple à utiliser si vous avez utilisé le crochet useReducer
de React. Il a une signature similaire. Voyons à quoi cela ressemble en termes de code.
import React from "react"; import { useImmerReducer } from "use-immer"; const initState = {} const reducer = (draft, action) => { switch(action.type) { default: break; } } const [data, dataDispatch] = useImmerReducer(reducer, initState);
Nous pouvons voir que le réducteur reçoit un état draft
que nous pouvons modifier autant que nous le voulons. Il y a aussi un exemple de codesandbox ici pour que vous puissiez expérimenter.
Et c'est comme ça qu'il est simple d'utiliser les crochets Immer. Mais au cas où vous vous demanderiez toujours pourquoi vous devriez utiliser Immer dans votre projet, voici un résumé de certaines des raisons les plus importantes que j'ai trouvées pour utiliser Immer.
Pourquoi devriez-vous utiliser Immer
Si vous avez écrit une logique de gestion d'état pendant un certain temps, vous apprécierez rapidement la simplicité qu'offre Immer. Mais ce n'est pas le seul avantage qu'offre Immer.
Lorsque vous utilisez Immer, vous finissez par écrire moins de code passe-partout comme nous l'avons vu avec des réducteurs relativement simples. Cela rend également les mises à jour approfondies relativement faciles.
Avec des bibliothèques telles que Immutable.js, vous devez apprendre une nouvelle API pour profiter des avantages de l'immuabilité. Mais avec Immer, vous obtenez la même chose avec des Objects
, des Arrays
, des Sets
et des Maps
JavaScript normaux . Il n'y a rien de nouveau à apprendre.
Immer fournit également un partage structurel par défaut. Cela signifie simplement que lorsque vous apportez des modifications à un objet d'état, Immer partage automatiquement les parties inchangées de l'état entre le nouvel état et l'état précédent.
Avec Immer, vous bénéficiez également d'un gel automatique des objets, ce qui signifie que vous ne pouvez pas modifier l'état produced
. Par exemple, lorsque j'ai commencé à utiliser Immer, j'ai essayé d'appliquer la méthode sort
sur un tableau d'objets renvoyés par la fonction Produce d'Immer. Il a lancé une erreur me disant que je ne peux apporter aucune modification au tableau. J'ai dû appliquer la méthode array slice avant d'appliquer sort
. Encore une fois, le nextState
produit est un arbre d'état immuable.
Immer est également fortement typé et très petit à seulement 3 Ko lorsqu'il est compressé.
Conclusion
En ce qui concerne la gestion des mises à jour d'état, l'utilisation d'Immer est une évidence pour moi. C'est une bibliothèque très légère qui vous permet de continuer à utiliser tout ce que vous avez appris sur JavaScript sans essayer d'apprendre quelque chose d'entièrement nouveau. Je vous encourage à l'installer dans votre projet et à commencer à l'utiliser immédiatement. Vous pouvez ajouter l'utiliser dans des projets existants et mettre à jour progressivement vos réducteurs.
Je vous encourage également à lire le billet de blog d'introduction Immer par Michael Weststrate. La partie que je trouve particulièrement intéressante est la section « Comment fonctionne Immer ? » section qui explique comment Immer exploite les fonctionnalités du langage telles que les proxies et les concepts tels que la copie sur écriture.
Je vous encourage également à jeter un œil à cet article de blog : Immutabilité en JavaScript : une vision contractuelle où l'auteur, Steven de Salas, présente ses réflexions sur les mérites de la poursuite de l'immuabilité.
J'espère qu'avec les choses que vous avez apprises dans cet article, vous pourrez commencer à utiliser Immer immédiatement.
Ressources associées
-
use-immer
, GitHub - Immerger, GitHub
-
function
, documentation Web MDN, Mozilla -
proxy
, documentation Web MDN, Mozilla - Objet (informatique), Wikipédia
- "Immuabilité dans JS", Orji Chidi Matthew, GitHub
- "Types de données et valeurs ECMAScript", Ecma International
- Collections immuables pour JavaScript, Immutable.js , GitHub
- "Le cas de l'immuabilité", Immutable.js , GitHub