Manejo del estado en React: cuatro enfoques inmutables a considerar

Quizás el punto de confusión más común en React hoy: estado.

Imagina que tienes un formulario para editar un usuario. Es común crear un solo controlador de cambios para manejar los cambios en todos los campos del formulario. Puede verse algo como esto:

updateState(event) { const {name, value} = event.target; let user = this.state.user; // this is a reference, not a copy... user[name] = value; // so this mutates state ? return this.setState({user}); }

La preocupación está en la línea 4. La línea 4 en realidad muta el estado porque la variable de usuario es una referencia al estado. El estado de reacción debe tratarse como inmutable.

De los documentos de React:

Nunca mutes this.statedirectamente, ya que llamar setState()después puede reemplazar la mutación que hiciste. Trátelo this.statecomo si fuera inmutable.

¿Por qué?

  1. Los lotes de setState funcionan entre bastidores. Esto significa que una mutación de estado manual puede anularse cuando se procesa setState.
  2. Si declara un método shouldComponentUpdate, no puede usar una verificación de igualdad === adentro porque la referencia del objeto no cambiará . Entonces, el enfoque anterior también tiene un impacto potencial en el rendimiento.

En pocas palabras : el ejemplo anterior a menudo funciona bien, pero para evitar casos extremos, trate el estado como inmutable.

Aquí hay cuatro formas de tratar el estado como inmutable:

Enfoque n. ° 1: asignación de objeto

Object.assign crea una copia de un objeto. El primer parámetro es el objetivo, luego especifica uno o más parámetros para las propiedades que le gustaría agregar. Entonces, arreglar el ejemplo anterior implica un simple cambio a la línea 3:

updateState(event) { const {name, value} = event.target; let user = Object.assign({}, this.state.user); user[name] = value; return this.setState({user}); }

En la línea 3, digo "Cree un nuevo objeto vacío y agregue todas las propiedades en this.state.user". Esto crea una copia separada del objeto de usuario que se almacena en estado. Ahora estoy seguro de mutar el objeto de usuario en la línea 4: es un objeto completamente separado del objeto en estado.

Asegúrese de polyfill Object.assign ya que no es compatible con IE y no lo transpila Babel. Cuatro opciones a considerar:

  1. asignar objeto
  2. Los documentos de MDN
  3. Polyfill de Babel
  4. Polyfill.io

Enfoque n. ° 2: propagación de objetos

La distribución de objetos es actualmente una característica de la etapa 3 y Babel puede transpilarla. Este enfoque es más conciso:

updateState(event) { const {name, value} = event.target; let user = {...this.state.user, [name]: value}; this.setState({user}); }

En la línea 3 estoy diciendo “Use todas las propiedades en this.state.user para crear un nuevo objeto, luego establezca la propiedad representada por [nombre] en un nuevo valor pasado en event.target.value”. Entonces, este enfoque funciona de manera similar al enfoque Object.assign, pero tiene dos beneficios:

  1. No se requiere polyfill, ya que Babel puede transpilar
  2. Mas conciso

Incluso puede usar desestructuración e inserción para hacer de esto una sola línea:

updateState({target}) { this.setState({user: {...this.state.user, [target.name]: target.value}}); }

Estoy desestructurando el evento en la firma del método para obtener una referencia a event.target. Luego declaro que el estado debe establecerse en una copia de this.state.user con la propiedad relevante establecida en un nuevo valor. Me gusta lo conciso que es esto. Este es actualmente mi enfoque favorito para escribir controladores de cambios. ?

Estos dos enfoques anteriores son las formas más comunes y sencillas de manejar el estado inmutable. ¿Quieres más potencia? Consulte las otras dos opciones a continuación.

Enfoque n. ° 3: Ayudante de inmutabilidad

Immutability-helper es una biblioteca útil para mutar una copia de datos sin cambiar la fuente. Esta biblioteca se sugiere en los documentos de React.

// Import at the top: import update from 'immutability-helper'; updateState({target}) { let user = update(this.state.user, {$merge: {[target.name]: target.value}}); this.setState({user}); }

En la línea 5, llamo merge, que es uno de los muchos comandos proporcionados por immutability-helper. Al igual que Object.assign, le paso el objeto de destino como el primer parámetro, luego especifico la propiedad en la que me gustaría fusionar.

El ayudante de inmutabilidad es mucho más que esto. Utiliza una sintaxis inspirada en el lenguaje de consulta de MongoDB y ofrece una variedad de formas poderosas de trabajar con datos inmutables.

Enfoque n. ° 4: Immutable.js

¿Quiere hacer cumplir la inmutabilidad mediante programación? Considere immutable.js. Esta biblioteca proporciona estructuras de datos inmutables.

Aquí hay un ejemplo, usando un mapa inmutable:

 // At top, import immutable import { Map } from 'immutable'; // Later, in constructor... this.state = { // Create an immutable map in state using immutable.js user: Map({ firstName: 'Cory', lastName: 'House'}) }; updateState({target}) { // this line returns a new user object assuming an immutable map is stored in state. let user = this.state.user.set(target.name, target.value); this.setState({user}); }

Hay tres pasos básicos arriba:

  1. Importar inmutable.
  2. Establecer estado en un mapa inmutable en el constructor
  3. Utilice el método set en el controlador de cambios para crear una nueva copia de usuario.

La belleza de immutable.js: si intenta mutar el estado directamente, fallará . Con los otros enfoques anteriores, es fácil de olvidar y React no te advertirá cuando mutes el estado directamente.

¿Las desventajas de inmutable?

  1. Hinchar . Immutable.js agrega 57K minificados a su paquete. Teniendo en cuenta que bibliotecas como Preact pueden reemplazar a React en solo 3K, eso es difícil de aceptar.
  2. Sintaxis . Debe hacer referencia a las propiedades del objeto a través de cadenas y llamadas a métodos en lugar de hacerlo directamente. Prefiero user.name sobre user.get ('nombre').
  3. YATTL (otra cosa más que aprender): cualquier persona que se una a su equipo necesita aprender otra API para obtener y configurar datos, así como un nuevo conjunto de tipos de datos.

Un par de otras alternativas interesantes a considerar:

  • impecable-inmutable
  • reaccionar-copiar-escribir

Warning: Watch Out For Nested Objects!

Option #1 and #2 above (Object.assign and Object spread) only do a shallow clone. So if your object contains nested objects, those nested objects will be copied by reference instead of by value. So if you change the nested object, you’ll mutate the original object. ?

Be surgical about what you’re cloning. Don’t clone all the things. Clone the objects that have changed. Immutability-helper (mentioned above) makes that easy. As do alternatives like immer, updeep, or a long list of alternatives.

You might be tempted to reach for deep merging tools like clone-deep, or lodash.merge, but avoid blindly deep cloning.

Here’s why:

  1. Deep cloning is expensive.
  2. Deep cloning is typically wasteful (instead, only clone what has actually changed)
  3. La clonación profunda provoca representaciones innecesarias ya que React piensa que todo ha cambiado cuando, de hecho, quizás solo ha cambiado un objeto secundario específico.

Gracias a Dan Abramov por las sugerencias que mencioné anteriormente:

No creo que cloneDeep () sea una buena recomendación. Puede resultar muy caro. Copie solo las partes que realmente cambiaron. Bibliotecas como immutability-helper (//t.co/YadMmpiOO8), updeep (//t.co/P0MzD19hcD) o immer (//t.co/VyRa6Cd4IP) ayudan con esto.

- Dan Abramov (@dan_abramov) 23 de abril de 2018

Consejo final: considere usar setState funcional

Otra arruga puede morderte:

setState () no muta inmediatamente this.state pero crea una transición de estado pendiente. Acceder a this.state después de llamar a este método puede devolver potencialmente el valor existente.

Dado que las llamadas setState se agrupan, un código como este conduce a un error:

updateState({target}) { this.setState({user: {...this.state.user, [target.name]: target.value}}); doSomething(this.state.user) // Uh oh, setState merely schedules a state change, so this.state.user may still have old value }

If you want to run code after a setState call has completed, use the callback form of setState:

updateState({target}) { this.setState((prevState) => { const updatedUser = {...prevState.user, [target.name]: target.value}; // use previous value in state to build new state... return { user: updatedUser }; // And what I return here will be set as the new state }, () => this.doSomething(this.state.user); // Now I can safely utilize the new state I've created to call other funcs... ); }

My Take

I admire the simplicity and light weight of option #2 above: Object spread. It doesn’t require a polyfill or separate library, I can declare a change handler on a single line, and I can be surgical about what has changed. ? Working with nested object structures? I currently prefer Immer.

Have other ways you like to handle state in React? Please chime in via the comments!

Looking for More on React? ⚛

I’ve authored multiple React and JavaScript courses on Pluralsight (free trial). My latest, “Creating Reusable React Components” just published! ?

Cory House is the author of multiple courses on JavaScript, React, clean code, .NET, and more on Pluralsight. He is principal consultant at reactjsconsulting.com, a Software Architect at VinSolutions, a Microsoft MVP, and trains software developers internationally on software practices like front-end development and clean coding. Cory tweets about JavaScript and front-end development on Twitter as @housecor.