Catégories
Plugin et site web

De meilleurs réducteurs avec Immer – Smashing Magazine

A propos de l'auteur

Développeur frontend génial qui aime tout le codage. Je suis un amoureux de la musique chorale et je travaille pour la rendre plus accessible au monde, un téléchargement à la…
Plus à propos
Chidi

Dans cet article, nous allons apprendre à utiliser Immer pour écrire des réducteurs. Lorsque nous travaillons avec React, nous maintenons beaucoup d'état. Pour mettre à jour notre état, nous devons écrire beaucoup de réducteurs. L'écriture manuelle des réducteurs entraîne un code gonflé où nous devons toucher presque toutes les parties de notre état. C'est fastidieux et sujet aux erreurs. Dans cet article, nous allons voir comment Immer apporte plus de simplicité au processus d'écriture des réducteurs d'état.

En 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 lorsque 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 minuscule bibliothèque JavaScript 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, jetons un coup d'œil sur l'immuabilité en JavaScript et pourquoi c'est important dans une application React.

La dernière norme ECMAScript (aka JavaScript) définit neuf types de données intégrés. De ces neuf types, six sont appelés primitive valeurs / types. Ces six primitives sont undefined, number, string, boolean, bigint, et symbol. Une simple vérification avec JavaScript typeof L'opérateur 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 

UNE primitive est une valeur qui n'est pas un objet et qui n'a pas de méthode. 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 a été créée. Ainsi, les primitives seraient immutable.

Les trois autres types sont null, object, et function. Nous pouvons également vérifier leurs types en utilisant le typeof opérateur.

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 d'objet spécial. Au cas où vous vous poseriez également des questions null et comment il est différent de undefined. undefined signifie simplement que nous n'avons pas défini de valeur pour une variable 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 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 devrait retourner un tableau, qui est un object type. Quand il ne trouve pas un tel objet, il retourne null. De retour undefined ça n'aurait pas de sens ici non plus.

Assez avec ça. Revenons à la discussion de l'immuabilité.

Selon les documents MDN:

"Tous les types, à l'exception des 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.

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;
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 dérive de a, modification de 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 de mémoire séparé, met 5 là-bas, et des points b à cet endroit.

Et les 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 changer la propriété de nom via une variable d le change également en 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 à elle. Puis, quand il exécute l'instruction d = c, le moteur JavaScript pointe simplement d au même endroit. Il ne crée pas de nouvel emplacement mémoire. Ainsi, toute modification des éléments dans d est implicitement une opération sur les éléments 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 souhaitez afficher le nom de l'utilisateur some name en lisant à partir d'une 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 new name. Si c et d étaient des primitifs, 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é.

Après avoir compris pourquoi nous avons besoin d'immuabilité dans une application React, voyons maintenant comment Immer aborde le problème avec son produce une fonction.

Immer's produce Une fonction

L'API principale d'Immer est très petite, et la fonction principale avec laquelle vous travaillerez est la produce une fonction. 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 un brouillon (identique, mais toujours une copie) de l'état dans lequel il effectue toutes les mises à jour prévues. Enfin, il produces un nouvel état immuable avec tous les changements appliqués.

Le schéma général de ce type de mise à jour d'état est le suivant:

// produce signature
produce(state, callback) => nextState

Voyons comment cela fonctionne dans la 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 une autre partie de l'État. Ça laisse initState intacte 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 de notre état est la pets tableau. le producenextState 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, voyons comment produce peut 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 dans une étape ultérieure, définir son installed la clé de true

const newPackage = { name: 'immer', installed: false };

Si nous devions le faire de la manière habituelle avec la syntaxe de propagation des objets et des tableaux 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 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

//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 produit au curry accepte une fonction comme premier argument et retourne un produit au curry qui ne requiert maintenant qu'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 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 useReducer crochet, nous n'avons pas besoin de passer l'état explicitement comme je l'ai fait ci-dessus parce qu'il s'occupe de cela.

Vous vous demandez peut-être, Immer obtiendrait 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: le useImmer et le useImmerReducer crochets. Voyons comment ils fonctionnent.

En utilisant le useImmer Et useImmerReducer Crochets

La meilleure description du useImmer hook 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 le draft peut être muté librement, jusqu'à la fin du producteur et les changements seront rendus immuables et deviendront le prochain état.

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 useImmer le crochet 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 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.

// make changes to data
updateData(draft => {
  // modify the draft as much as you want.
})

Le créateur d'Immer a fourni un exemple de code et de boîte avec lequel vous pouvez jouer pour voir comment cela fonctionne.

useImmerReducer est également simple à utiliser si vous avez utilisé useReducer crochet. 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);

On voit que le réducteur reçoit un draft état que nous pouvons modifier autant que nous voulons. Vous pouvez également tester un exemple de code et de boîte ici.

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 utiliser Immer

Si vous avez écrit une logique de gestion des états pour une durée quelconque, 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 JavaScript normal Objects, Arrays, Sets, et Maps. 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 le gel automatique des objets, ce qui signifie que vous ne pouvez pas modifier produced Etat. Par exemple, lorsque j'ai commencé à utiliser Immer, j'ai essayé d'appliquer le sort sur un tableau d'objets retournés par la fonction produit 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 sort. 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, 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 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 blog de présentation 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 la copie sur écriture.

Je vous encourage également à consulter cet article de blog: Immuabilité en JavaScript: une vue contratienne 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.

  1. use-immer, GitHub
  2. Immer, GitHub
  3. function, Documents Web MDN, Mozilla
  4. proxy, Documents Web MDN, Mozilla
  5. Objet (informatique), Wikipedia
  6. «Immuabilité dans JS», Orji Chidi Matthew, GitHub
  7. «Types et valeurs des données ECMAScript», Ecma International
  8. Collections immuables pour JavaScript, Immutable.js, GitHub
  9. «Le cas de l'immuabilité», Immutable.js, GitHub
Smashing Editorial(ks, ra, il)

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *