Logotipo de Zephyrnet

Mejores reductores con Immer

Fecha:

Sobre el Autor

Impresionante desarrollador frontend que ama todo lo relacionado con la codificación. Soy un amante de la música coral y estoy trabajando para hacerla más accesible al mundo, una carga en un ...
Más información sobre
Chidi
...

En este artículo, aprenderemos cómo usar Immer para escribir reductores. Cuando trabajamos con React, mantenemos mucho estado. Para hacer actualizaciones a nuestro estado, necesitamos escribir muchos reductores. La escritura manual de reductores da como resultado un código inflado en el que tenemos que tocar casi todas las partes de nuestro estado. Esto es tedioso y propenso a errores. En este artículo, veremos cómo Immer aporta más simplicidad al proceso de escritura de reductores de estado.

Como desarrollador de React, ya debería estar familiarizado con el principio de que El estado no debe mutarse directamente. Quizás se esté preguntando qué significa eso (la mayoría de nosotros teníamos esa confusión cuando comenzamos).

Este tutorial hará justicia a eso: comprenderá qué es el estado inmutable y su necesidad. También aprenderá a usar Immer para trabajar con estado inmutable y los beneficios de usarlo.
Puedes encontrar el código en este artículo en este Github repo.

Inmutabilidad en JavaScript y por qué es importante

Immer.js es una pequeña biblioteca de JavaScript escrita por Michel Weststrate cuya misión declarada es permitirle "trabajar con un estado inmutable de una manera más conveniente".

Pero antes de sumergirnos en Immer, repasemos rápidamente la inmutabilidad en JavaScript y por qué es importante en una aplicación React.

El último estándar ECMAScript (también conocido como JavaScript) define nueve tipos de datos integrados. De estos nueve tipos, hay seis que se denominan primitive valores / tipos. Estos seis primitivos son undefined, number, string, boolean, biginty symbol. Una simple comprobación con JavaScript typeof El operador revelará los tipos de estos tipos de datos.

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

A primitive es un valor que no es un objeto y no tiene métodos. Lo más importante para nuestra discusión actual es el hecho de que el valor de un primitivo no se puede cambiar una vez creado. Por tanto, se dice que las primitivas son immutable.

Los tres tipos restantes son null, objecty function. También podemos comprobar sus tipos utilizando el typeof operador.

console.log(typeof null) // object
console.log(typeof [0, 1]) // object
console.log(typeof {name: 'name'}) // object
const f = () => ({})
console.log(typeof f) // function

Estos tipos son mutable. Esto significa que sus valores se pueden cambiar en cualquier momento después de su creación.

Quizás se pregunte por qué tengo la matriz [0, 1] allí arriba. Bueno, en JavaScriptland, una matriz es simplemente un tipo especial de objeto. En caso de que también te estés preguntando null y en que se diferencia de undefined. undefined simplemente significa que no hemos establecido un valor para una variable mientras null es un caso especial para objetos. Si sabe que algo debería ser un objeto pero el objeto no está allí, simplemente regrese null.

Para ilustrarlo con un ejemplo simple, intente ejecutar el siguiente código en la consola de su navegador.

console.log('aeiou'.match(/[x]/gi)) // null
console.log('xyzabc'.match(/[x]/gi)) // [ 'x' ]

String.prototype.match debería devolver una matriz, que es una object escribe. Cuando no puede encontrar tal objeto, regresa null. Volviendo undefined aquí tampoco tendría sentido.

Basta con eso. Volvamos a discutir la inmutabilidad.

Según los documentos de MDN:

"Todos los tipos, excepto los objetos, definen valores inmutables (es decir, valores que no se pueden cambiar)".

Esta declaración incluye funciones porque son un tipo especial de objeto JavaScript. Ver definición de función esta página.

Echemos un vistazo rápido a lo que significan en la práctica los tipos de datos mutables e inmutables. Intente ejecutar el siguiente código en la consola de su navegador.

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

Nuestros resultados muestran que aunque b se deriva de a, cambiando el valor de b no afecta el valor de a. Esto surge del hecho de que cuando el motor JavaScript ejecuta la declaración b = a, crea una nueva ubicación de memoria separada, coloca 5 ahí dentro, y apunta b en ese lugar

¿Qué pasa con los objetos? Considere el siguiente código.

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"}

Podemos ver que cambiando la propiedad del nombre a través de la variable d también lo cambia en c. Esto surge del hecho de que cuando el motor JavaScript ejecuta la declaración, c = { name: 'some name' }, el motor JavaScript crea un espacio en la memoria, coloca el objeto dentro y apunta c en eso. Entonces, cuando ejecuta la sentencia d = c, el motor de JavaScript solo apunta d a la misma ubicación. No crea una nueva ubicación de memoria. Por lo tanto, cualquier cambio en los elementos en d es implícitamente una operación sobre los elementos en c. Sin mucho esfuerzo, podemos ver por qué esto es un problema en ciernes.

Imagine que está desarrollando una aplicación React y en algún lugar desea mostrar el nombre del usuario como some name leyendo de la variable c. Pero en otro lugar habías introducido un error en tu código al manipular el objeto d. Esto daría como resultado que el nombre del usuario apareciera como new name. Si c y d si fuéramos primitivos no tendríamos ese problema. Pero las primitivas son demasiado simples para los tipos de estado que debe mantener una aplicación React típica.

Se trata de las principales razones por las que es importante mantener un estado inmutable en su aplicación. Le animo a que consulte algunas otras consideraciones al leer esta breve sección del README de Immutable.js: el caso de la inmutabilidad.

Habiendo entendido por qué necesitamos la inmutabilidad en una aplicación React, ahora echemos un vistazo a cómo Immer aborda el problema con su produce función.

Immer's produce Función

La API central de Immer es muy pequeña y la función principal con la que trabajará es la produce función. produce simplemente toma un estado inicial y una devolución de llamada que define cómo se debe mutar el estado. La devolución de llamada en sí recibe una copia en borrador (idéntica, pero aún una copia) del estado en el que realiza toda la actualización deseada. Finalmente, es produceUn estado nuevo e inmutable con todos los cambios aplicados.

El patrón general para este tipo de actualización de estado es:

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

Veamos cómo funciona esto en la práctica.

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)
})

En el código anterior, simplemente pasamos el estado inicial y una devolución de llamada que especifica cómo queremos que ocurran las mutaciones. Es tan simple como eso. No necesitamos tocar ninguna otra parte del estado. Se va initState intacta y comparte estructuralmente aquellas partes del estado que no tocamos entre el estado inicial y el nuevo. Una de esas partes en nuestro estado es la pets formación. los produced nextState es un árbol de estado inmutable que tiene los cambios que hemos realizado, así como las partes que no modificamos.

Armados con este conocimiento simple pero útil, echemos un vistazo a cómo produce puede ayudarnos a simplificar nuestros reductores React.

Reductores de escritura con Immer

Supongamos que tenemos el objeto de estado definido a continuación

const initState = { pets: ['dog', 'cat'], packages: [ { name: 'react', installed: true }, { name: 'redux', installed: true }, ],
};

Y queríamos agregar un nuevo objeto y, en un paso posterior, establecer su installed clave para true

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

Si hiciéramos esto de la forma habitual con el objeto JavaScripts y la sintaxis de extensión de matriz, nuestro reductor de estado podría verse como a continuación.

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; }
};

Podemos ver que esto es innecesariamente detallado y propenso a errores para este objeto de estado relativamente simple. También tenemos que tocar cada parte del estado, lo cual es innecesario. Veamos cómo podemos simplificar esto con 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; } });

Y con unas pocas líneas de código, hemos simplificado enormemente nuestro reductor. Además, si caemos en el caso predeterminado, Immer simplemente devuelve el estado de borrador sin que tengamos que hacer nada. Observe cómo hay menos código repetitivo y la eliminación de la propagación estatal. Con Immer, solo nos preocupamos por la parte del estado que queremos actualizar. Si no podemos encontrar un elemento de este tipo, como en la acción `UPDATE_INSTALLED`, simplemente seguimos adelante sin tocar nada más.
La función "producir" también se presta para curar. Pasar una devolución de llamada como primer argumento para "producir" está destinado a ser utilizado para curry. La firma del "producto" al curry es

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

Veamos cómo podemos actualizar nuestro estado anterior con un producto al curry. Nuestro producto al curry se vería así:

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 función de productos con curry acepta una función como su primer argumento y devuelve un producto con curry que solo ahora requiere un estado a partir del cual producir el siguiente estado. El primer argumento de la función es el estado de borrador (que se derivará del estado que se pasará al llamar a este producto con curry). Luego sigue cada número de argumentos que deseamos pasar a la función.

Todo lo que tenemos que hacer ahora para usar esta función es pasar el estado desde el que queremos producir el siguiente estado y el objeto de acción como tal.

// 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,
});

Tenga en cuenta que en una aplicación React cuando se usa el useReducer hook, no necesitamos pasar el estado explícitamente como lo hice anteriormente porque se encarga de eso.

Tal vez se pregunte, ¿Immer estaría obteniendo un hook, como todo en React estos días? Bueno, estás en compañía de buenas noticias. Immer tiene dos ganchos para trabajar con el estado: el useImmer y del useImmerReducer manos. Veamos como funcionan.

Utilizando la useImmer Y useImmerReducer Manos

La mejor descripción del useImmer hook proviene del propio README de use-immer.

useImmer(initialState) es muy similar a useState. La función devuelve una tupla, el primer valor de la tupla es el estado actual, el segundo es la función de actualización, que acepta una función de productor immer, En la que el draft se puede mutar libremente, hasta que el productor termine y los cambios se hagan inmutables y se conviertan en el siguiente estado.

Para hacer uso de estos ganchos, debe instalarlos por separado, además de la biblioteca principal de Immer.

yarn add immer use-immer

En términos de código, el useImmer gancho se ve como abajo

import React from "react";
import { useImmer } from "use-immer"; const initState = {}
const [ data, updateData ] = useImmer(initState)

Y es tan simple como eso. Se podría decir que es useState de React pero con un poco de esteroide. Utilizar la función de actualización es muy sencillo. Recibe el estado de borrador y puedes modificarlo tanto como quieras como a continuación.

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

El creador de Immer ha proporcionado una codigos y caja ejemplo con el que puedes jugar para ver cómo funciona.

useImmerReducer es igualmente simple de usar si ha usado React's useReducer gancho. Tiene una firma similar. Veamos cómo se ve eso en términos de código.

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);

Podemos ver que el reductor recibe un draft Estado que podemos modificar tanto como queramos. También hay un codigos y caja ejemplo aquí para que experimente.

Y así de sencillo es utilizar los ganchos Immer. Pero en caso de que todavía se esté preguntando por qué debería usar Immer en su proyecto, aquí hay un resumen de algunas de las razones más importantes que he encontrado para usar Immer.

Por qué debería usar Immer

Si ha escrito la lógica de administración de estado durante un período de tiempo, apreciará rápidamente la simplicidad que ofrece Immer. Pero ese no es el único beneficio que ofrece Immer.

Cuando usa Immer, termina escribiendo menos código repetitivo como hemos visto con reductores relativamente simples. Esto también hace que las actualizaciones profundas sean relativamente fáciles.

Con bibliotecas como Immutable.js, tienes que aprender una nueva API para aprovechar los beneficios de la inmutabilidad. Pero con Immer logras lo mismo con JavaScript normal Objects, Arrays, Setsy Maps. No hay nada nuevo que aprender.

Immer también proporciona uso compartido estructural de forma predeterminada. Esto simplemente significa que cuando realiza cambios en un objeto de estado, Immer comparte automáticamente las partes sin cambios del estado entre el nuevo estado y el estado anterior.

Con Immer, también obtiene la congelación automática de objetos, lo que significa que no puede realizar cambios en el produced estado. Por ejemplo, cuando comencé a usar Immer, intenté aplicar el sort en una matriz de objetos devueltos por la función de producción de Immer. Lanzó un error diciéndome que no puedo realizar ningún cambio en la matriz. Tuve que aplicar el método de corte de matriz antes de aplicar sort. Una vez más, el producido nextState es un árbol de estado inmutable.

Immer también está fuertemente tipado y es muy pequeño con solo 3 KB cuando se comprime con gzip.

Conclusión

Cuando se trata de administrar actualizaciones de estado, usar Immer es una obviedad para mí. Es una biblioteca muy liviana que le permite seguir usando todo lo que ha aprendido sobre JavaScript sin intentar aprender algo completamente nuevo. Te animo a que lo instales en tu proyecto y empieces a usarlo de inmediato. Puede agregar usarlo en proyectos existentes y actualizar gradualmente sus reductores.

También te animo a leer el Entrada de blog introductoria de Immer por Michael Weststrate. La parte que encuentro especialmente interesante es el "¿Cómo funciona Immer?" sección que explica cómo Immer aprovecha las características del lenguaje como proxies y conceptos como Copiar en escrito.

También te animo a que eches un vistazo a esta publicación de blog: Inmutabilidad en JavaScript: una visión contraria donde el autor, Steven de Salas, presenta sus pensamientos sobre los méritos de perseguir la inmutabilidad.

Espero que, con las cosas que ha aprendido en esta publicación, pueda comenzar a usar Immer de inmediato.

  1. use-immer, GitHub
  2. Eternidad, GitHub
  3. function, Documentos web de MDN, Mozilla
  4. proxy, Documentos web de MDN, Mozilla
  5. Objeto (informática), Wikipedia
  6. "Inmutabilidad en JS, ”Orji Chidi Matthew, GitHub
  7. "Valores y tipos de datos de ECMAScript, ”Ecma International
  8. Colecciones inmutables para JavaScript, Immutable.js, GitHub
  9. "El caso de la inmutabilidad, ”Immutable.js, GitHub
Editorial sensacional
(ks, ra, il)

Fuente: https://www.smashingmagazine.com/2020/06/better-reducers-with-immer/

punto_img

Información más reciente

punto_img