De meilleurs réducteurs avec Immer
En tant que développeur React, vous devez déjà être familier 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 un é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 dépôt Github .
Immuabilité en JavaScript et pourquoi c'est important
Immer.js est une petite bibliothèque JavaScript qui a été écrite par Michel Weststrate dont la mission déclarée est de vous permettre de "travailler avec un état immuable d'une manière plus pratique."
Mais avant de plonger dans Immer, passons rapidement à un rappel sur l'immuabilité en JavaScript et pourquoi c'est important dans une application React. [19659005] La dernière norme ECMAScript (aka JavaScript) définit neuf types de données intégrés. De ces neuf types, il y en a six qui sont appelés valeurs / types primitifs
. Ces six primitives sont non définies
nombre
chaîne
booléen
bigint
et symbole
et
symbole
. Une simple vérification avec l'opérateur JavaScript
typeof
révélera les types de ces types de données.
console.log (typeof 5) // number
console.log (typeof 'nom') // chaîne
console.log (typeof (1
Une primitive
est une valeur qui n'est pas un objet et 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 Ainsi, les primitives seraient immuables
.
Les trois types restants sont nuls
objet
et fonction
. Nous pouvons également vérifier leurs types à l'aide de l'opérateur typeof
.
console.log (typeof null) // object
console.log (typeof [0, 1]) // objet
console.log (typeof {name: 'name'}) // objet
const f = () => ({})
console.log (typeof f) // fonction
Ces types sont mutables
. 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 d'objet spécial. Au cas où vous vous poseriez également des questions sur null
et en quoi il diffère de undefined
. undefined
signifie simplement que nous n'avons pas défini de valeur pour une variable tandis que null
est un cas spécial pour les objets. Si vous savez que quelque chose doit être un objet mais que l'objet n'est pas là, vous retournez 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'objet
. Lorsqu'il ne trouve pas un tel objet, il renvoie null
. Retourner non défini
n'aurait pas de sens ici non plus.
Ça suffit. Revenons à la discussion de 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 elles sont un type spécial d'objet JavaScript. Voir la définition de fonction ici .
Voyons rapidement ce que les types de données mutables et immuables signifient dans la pratique. Essayez d'exécuter le code ci-dessous dans la console de votre navigateur.
let a = 5;
soit 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
la modification de la valeur de b
n'affecte pas la valeur de a
. Cela provient du fait que lorsque le moteur JavaScript exécute l'instruction b = a
il crée un nouvel emplacement de mémoire séparé, y place 5
et pointe b
à cet endroit.
Et les objets? Considérez le code ci-dessous.
let c = {name: 'some name'}
soit d = c;
console.log (`c: $ {JSON.stringify (c)}; d: $ {JSON.stringify (d)}`) // {"name": "some name"}; d: {"nom": "un nom"}
d.name = 'nouveau nom'
console.log (`c: $ {JSON.stringify (c)}; d: $ {JSON.stringify (d)}`) // {"nom": "nouveau nom"}; d: {"nom": "nouveau nom"}
Nous pouvons voir que la modification de la propriété de nom via la variable d
la change également dans c
. Cela vient du fait que lorsque le moteur JavaScript exécute l'instruction, c = {nom: 'un nom
'
}
le moteur JavaScript crée un espace en mémoire, place l'objet à l'intérieur et pointe c
dessus. Puis, 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 cela pose problème.
Imaginez que vous développiez une application React et quelque part où vous voulez montrer le nom de l'utilisateur comme un nom
en lisant la variable c
. Mais ailleurs, vous aviez introduit un bug dans votre code en manipulant l'objet d
. Cela entraînerait le nom de l'utilisateur apparaissant comme nouveau nom
. 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 README Immutable.js: le cas de l'immuabilité .
Ayant compris pourquoi nous avons besoin de l'immuabilité dans une application React, prenons maintenant un aperçu de la façon dont Immer aborde le problème avec sa fonction produire
.
Produire d'Immer
Fonction
L'API de base d'Immer est très petite et la fonction principale sur laquelle vous travaillerez avec la fonction produire
. produit
prend simplement un état initial et un rappel qui définit comment l'état doit être muté. Le rappel lui-même reçoit un brouillon (identique, mais toujours une copie) de l'état dans lequel il effectue toutes les mises à jour prévues. Enfin, il produit
un nouvel état immuable avec toutes les modifications appliquées.
Le schéma général de ce type de mise à jour d'état est le suivant:
// produit la signature
produire (état, rappel) => nextState
Voyons comment cela fonctionne dans la pratique.
importez des produits de "immer"
const initState = {
animaux de compagnie: ['dog', 'cat'],
packages: [
{ name: 'react', installed: true },
{ name: 'redux', installed: true },
],
}
// pour ajouter un nouveau package
const newPackage = {nom: 'immer', installé: faux}
const nextState = produire (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 une 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 les États de départ et les nouveaux États. Une telle partie dans notre état est le tableau animaux de compagnie
. Le produit
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é de ce simple mais utile connaissances, examinons comment produire
peut nous aider à simplifier nos réducteurs React.
Rédaction de réducteurs avec Immer
Supposons que nous ayons l'objet d'état défini ci-dessous
const initState = {
animaux de compagnie: ['dog', 'cat'],
packages: [
{ name: 'react', installed: true },
{ name: 'redux', installed: true },
],
};
Et nous voulions ajouter un nouvel objet, et dans une étape ultérieure, définir sa clé installée
sur true
const newPackage = {name: 'immer', installé : faux };
Si nous procédions de la manière habituelle avec la syntaxe de répartition des objets et des tableaux JavaScripts, notre réducteur d'état pourrait ressembler à ce qui suit.
const updateReducer = (state = initState, action) => {
commutateur (action.type) {
cas 'ADD_PACKAGE':
revenir {
...Etat,
packages: [...state.packages, action.package],
};
cas 'UPDATE_INSTALLED':
revenir {
...Etat,
packages: state.packages.map (pack =>
pack.name === action.name
? {... pack, installé: action.installed}
: pack
),
};
défaut:
état de retour;
}
};
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 simplifier cela avec Immer.
const updateReducerWithProduce = (state = initState, action) =>
produire (état, brouillon => {
commutateur (action.type) {
cas 'ADD_PACKAGE':
draft.packages.push (action.package);
Pause;
cas "UPDATE_INSTALLED": {
const package = draft.packages.filter (p => p.name === action.name) [0];
if (package) package.installed = action.installed;
Pause;
}
défaut:
Pause;
}
});
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 de l'état. Avec Immer, nous ne nous intéressons qu'à la partie de l'Etat que nous voulons mettre à jour. Si nous ne pouvons pas trouver un tel élément, comme dans l'action `UPDATE_INSTALLED`, nous allons simplement de l'avant sans toucher à rien d'autre.
La fonction `produit` 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
// signature du produit au curry
produire (rappel) => (état) => nextState
Voyons comment nous pouvons mettre à jour notre état antérieur avec un produit au curry. Notre produit au curry ressemblerait à ceci:
const curriedProduce = produit ((brouillon, action) => {
commutateur (action.type) {
cas 'ADD_PACKAGE':
draft.packages.push (action.package);
Pause;
cas 'SET_INSTALLED': {
const package = draft.packages.filter (p => p.name === action.name) [0];
if (package) package.installed = action.installed;
Pause;
}
défaut:
Pause;
}
});
La fonction de production au curry accepte une fonction comme premier argument et renvoie un produit au curry qui n'a besoin que d'un état à partir duquel produire l'état suivant. Le premier argument de la fonction est le projet d'état (qui sera dérivé de l'état à passer lors de l'appel de ce produit au curry). Suit ensuite chaque nombre d'arguments que nous souhaitons passer à la fonction.
Tout ce que nous devons faire maintenant pour utiliser cette fonction est de passer dans l'état à partir duquel nous voulons produire l'état suivant et l'objet action comme ceci. [19659010] // ajoute un nouveau package à l'état de départ
const nextState = curriedProduce (initState, {
tapez: 'ADD_PACKAGE',
package: newPackage,
});
// met à jour un élément dans l'état récemment produit
const nextState2 = curriedProduce (nextState, {
tapez: 'SET_INSTALLED',
nom: «immer»,
installé: vrai,
});
Notez que dans une application React lors de l'utilisation du crochet useReducer
nous n'avons pas besoin de passer l'état explicitement comme je l'ai fait ci-dessus car il s'en occupe.
Vous pourriez être se demandant, Immer recevrait-il un crochet
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 crochets useImmer
et useImmerReducer
. Voyons comment ils fonctionnent.
Utilisation du useImmer
Et useImmerReducer
Crochets
La meilleure description du crochet useImmer
provient du fichier README immergé.
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 le brouillonpeut être muté librement, jusqu'à ce que le producteur se termine et que les modifications deviennent immuables et deviennent le prochain état.
Pour utiliser ces crochets, vous devez les installer séparément, en plus du libaraire principal Immer.
yarn add immer use-immer
En termes de code, le crochet useImmer
ressemble ci-dessous
à l'importation React de "react";
importer {useImmer} à partir de "use-immer";
const initState = {}
const [ data, updateData ] = useImmer (initState)
Et c'est aussi simple que ça. On peut dire que c'est React's useState mais avec un peu de stéroïde. L'utilisation de 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.
// apporter des modifications aux données
updateData (draft => {
// modifiez le brouillon autant que vous le souhaitez.
})
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 l'avez utilisé Le crochet useReducer de React . Il a une signature similaire. Voyons à quoi cela ressemble en termes de code.
import React from "react";
importer {useImmerReducer} depuis "use-immer";
const initState = {}
réducteur de const = (brouillon, action) => {
commutateur (action.type) {
défaut:
Pause;
}
}
const [data, dataDispatch] = useImmerReducer (réducteur, initState);
On voit que le réducteur reçoit un projet
d'état que l'on peut modifier autant qu'on veut. Il y a aussi un exemple de codesandbox ici pour vous permettre d'expérimenter.
Et c'est aussi simple que d'utiliser des 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 vous devriez utiliser Immer
Si vous avez une logique de gestion d'état écrite pour n'importe quelle durée, 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 profondes 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 objets JavaScript
tableaux
ensembles
et cartes
normaux. Il n'y a rien de nouveau à apprendre.
Immer fournit également le 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 obtenez également un gel d'objet automatique, ce qui signifie que vous ne pouvez pas apporter de modifications à l'état produit
. Par exemple, lorsque j'ai commencé à utiliser Immer, j'ai essayé d'appliquer la méthode de tri
à un tableau d'objets renvoyé par la fonction de production d'Immer. Cela a provoqué une erreur me disant que je ne pouvais pas apporter de modifications au tableau. J'ai dû appliquer la méthode de tranche de tableau avant d'appliquer le tri
. Encore une fois, le produit nextState
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, utiliser 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 de complètement 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 de Michael Weststrate. La partie que je trouve particulièrement intéressante est «Comment fonctionne Immer?» section qui explique comment Immer tire parti des fonctionnalités du langage telles que les proxys et des concepts tels que copie sur écriture .
Je vous encourage également à consulter ce billet de blog : Immuabilité en JavaScript: une vue contractienne 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 ce post, vous pouvez commencer à utiliser Immer immédiatement.
Ressources connexes
use-immer
GitHub- Immer fonction GitHub
proxy
]Documents Web MDN, Mozilla- Objet (informatique) Wikipedia
- « Immuabilité dans JS », Orji Chidi Matthew, GitHub
- « Types de données ECMAScript and Values », Ecma International
- Collections immuables pour JavaScript Immutable.js, GitHub
- « Le cas de l'immuabilité », Immutable.js, GitHub

Source link