Comprensión de la inmutabilidad en JavaScript | Programar Plus

Si no ha trabajado antes con la inmutabilidad en JavaScript, es posible que le resulte fácil confundirlo con la asignación de una variable a un nuevo valor o reasignación. Si bien es posible reasignar variables y valores declarados usando let o var, comenzará a tener problemas cuando intente eso con const.

Digamos que asignamos el value Kingsley a una variable llamada firstName:

let firstName = "Kingsley";

Podemos reasignar un nuevo valor a la misma variable,

firstName = "John";

Esto es posible porque usamos let. Si usamos const en lugar de esto:

const lastName = "Silas";

… obtendremos un error cuando intentemos asignarle un nuevo valor;

lastName = "Doe"
// TypeError: Assignment to constant variable.

Eso no es inmutabilidad.

Un concepto importante que escuchará trabajando con un marco, como React, es que los estados mutantes es una mala idea. Lo mismo se aplica a los accesorios. Sin embargo, es importante saber que la inmutabilidad no es un concepto de React. React pasa a hacer uso de la idea de inmutabilidad cuando se trabaja con cosas como el estado y los accesorios.

¿Qué diablos significa eso? Ahí es donde vamos a retomar las cosas.

La mutabilidad se trata de ceñirse a los hechos

Los datos inmutables no pueden cambiar su estructura o los datos que contienen. Es establecer un valor en una variable que no puede cambiar, hacer que ese valor sea un hecho, o una especie de fuente de verdad, de la misma manera que una princesa besa a una rana con la esperanza de que se convierta en un príncipe apuesto. La inmutabilidad dice que la rana siempre será una rana.

Los objetos y las matrices, por otro lado, permiten la mutación, lo que significa que la estructura de datos se puede cambiar. Besar a cualquiera de esas ranas puede resultar en la transformación de un príncipe si se lo decimos.

Digamos que tenemos un objeto de usuario como este:

let user = { name: "James Doe", location: "Lagos" }

A continuación, intentemos crear un newUser objeto usando esas propiedades:

let newUser = user

Ahora imaginemos que el primer usuario cambia de ubicación. Mutará directamente el user objeto y afectar el newUser también:

user.location = "Abia"
console.log(newUser.location) // "Abia"

Puede que esto no sea lo que queremos. Puede ver cómo este tipo de reasignación podría tener consecuencias no deseadas.

Trabajar con objetos inmutables

Queremos asegurarnos de que nuestro objeto no esté mutado. Si vamos a utilizar un método, debe devolver un nuevo objeto. En esencia, necesitamos algo llamado función pura.

Una función pura tiene dos propiedades que la hacen única:

  1. El valor que devuelve depende de la entrada pasada. El valor devuelto no cambiará mientras las entradas no cambien.
  2. No cambia cosas fuera de su alcance.

Mediante el uso Object.assign(), podemos crear una función que no mute el objeto que se le pasa. Esto generará un nuevo objeto copiando el segundo y tercer parámetro en el objeto vacío pasado como primer parámetro. Luego se devuelve el nuevo objeto.

const updateLocation = (data, newLocation) => {
    return {
      Object.assign({}, data, {
        location: newLocation
    })
  }
}

updateLocation() es una función pura. Si pasamos en el primero user objeto, devuelve un nuevo user objeto con un nuevo valor para la ubicación.

Otra forma de hacerlo es usar el operador Spread:

const updateLocation = (data, newLocation) => {
  return {
    ...data,
    location: newLocation
  }
}

Bien, entonces, ¿cómo encaja todo esto en React? Entremos en eso a continuación.

Inmutabilidad en React

En una aplicación típica de React, el estado es un objeto. (Redux hace uso de un objeto inmutable como base de la tienda de una aplicación). El proceso de reconciliación de React determina si un componente debe volver a renderizarse o si necesita una forma de realizar un seguimiento de los cambios.

En otras palabras, si React no puede darse cuenta de que el estado de un componente ha cambiado, entonces no sabrá actualizar el DOM virtual.

La inmutabilidad, cuando se aplica, permite realizar un seguimiento de esos cambios. Esto permite a React comparar el estado anterior si un objeto con su nuevo estado y volver a renderizar el componente en función de esa diferencia.

Esta es la razón por la que a menudo se desaconseja actualizar directamente el estado en React:

this.state.username = "jamesdoe";

React no se asegurará de que el estado haya cambiado y no pueda volver a renderizar el componente.

Immutable.js

Redux se adhiere a los principios de inmutabilidad. Sus reductores están destinados a ser funciones puras y, como tales, no deben mutar el estado actual, sino devolver un nuevo objeto basado en el estado y la acción actuales. Normalmente haríamos uso del operador de propagación como lo hicimos antes, pero es posible lograr lo mismo usando una biblioteca llamada Immutable.js.

Si bien JavaScript simple puede manejar la inmutabilidad, es posible encontrarse con un puñado de trampas en el camino. El uso de Immutable.js garantiza la inmutabilidad al tiempo que proporciona una API rica que ofrece un gran rendimiento. No entraremos en todos los detalles de Immutability.js en este artículo, pero veremos un ejemplo rápido que demuestra su uso en una aplicación de tareas pendientes impulsada por React y Redux.

Primero, comencemos por importar los módulos que necesitamos y configurar el Todo componente mientras estamos en eso.


const { List, Map } = Immutable;
const { Provider, connect } = ReactRedux;
const { createStore } = Redux;

Si está siguiendo a lo largo de su máquina local. necesitará tener estos paquetes instalados:

npm install redux react-redux immutable 

Las declaraciones de importación se verán así.

import { List, Map } from "immutable";
import { Provider, connect } from "react-redux";
import { createStore } from "redux";

Luego podemos continuar para configurar nuestro Todo componente con algún marcado:

const Todo = ({ todos, handleNewTodo }) => {
  const handleSubmit = event => {
    const text = event.target.value;
    if (event.keyCode === 13 && text.length > 0) {
      handleNewTodo(text);
      event.target.value = "";
    }
  };

  return (
    <section className="section">
      <div className="box field">
        <label className="label">Todo</label>
        <div className="control">
          <input
            type="text"
            className="input"
            placeholder="Add todo"
            onKeyDown={handleSubmit}
          />
        </div>
      </div>
      <ul>
        {todos.map(item => (
          <div key={item.get("id")} className="box">
            {item.get("text")}
          </div>
        ))}
      </ul>
    </section>
  );
};

Estamos usando el handleSubmit() método para crear nuevos elementos de tareas pendientes. Para el propósito de este ejemplo, el usuario solo creará nuevos elementos para hacer y solo necesitamos una acción para eso:

const actions = {
  handleNewTodo(text) {
    return {
      type: "ADD_TODO",
      payload: {
        id: uuid.v4(),
        text
      }
    };
  }
};

El payload que estamos creando contiene la ID y el texto de la tarea pendiente. Luego podemos continuar para configurar nuestra función reductora y pasar la acción que creamos anteriormente a la función reductora:

const reducer = function(state = List(), action) {
  switch (action.type) {
    case "ADD_TODO":
      return state.push(Map(action.payload));
    default:
      return state;
  }
};

Vamos a hacer uso de connect para crear un componente contenedor para que podamos conectarnos a la tienda. Entonces tendremos que pasar mapStateToProps() y mapDispatchToProps() funciones para connect.

const mapStateToProps = state => {
  return {
    todos: state
  };
};

const mapDispatchToProps = dispatch => {
  return {
    handleNewTodo: text => dispatch(actions.handleNewTodo(text))
  };
};

const store = createStore(reducer);

const App = connect(
  mapStateToProps,
  mapDispatchToProps
)(Todo);

const rootElement = document.getElementById("root");

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  rootElement
);

Estamos haciendo uso de mapStateToProps() para suministrar al componente los datos de la tienda. Entonces estamos usando mapDispatchToProps() para que los creadores de acciones estén disponibles como accesorios para el componente vinculando la acción a él.

En la función reductora utilizamos List de Immutable.js para crear el estado inicial de la aplicación.

const reducer = function(state = List(), action) {
  switch (action.type) {
    case "ADD_TODO":
      return state.push(Map(action.payload));
    default:
      return state;
  }
};

Pensar en List como una matriz de JavaScript, por lo que podemos hacer uso de la .push() método en el estado. El valor utilizado para actualizar el estado es un objeto que continúa diciendo que Map puede ser reconocido como un objeto. De esta manera, no es necesario usar Object.assign() o el operador de propagación, ya que garantiza que el estado actual no puede cambiar. Esto parece mucho más limpio, especialmente si resulta que el estado está profundamente anidado; no es necesario que los operadores de propagación se esparzan por todas partes.

Los estados inmutables hacen posible que el código determine rápidamente si se ha producido un cambio. No es necesario que hagamos una comparación recursiva de los datos para determinar si ocurrió un cambio. Dicho esto, es importante mencionar que es posible que tenga problemas de rendimiento cuando trabaje con grandes estructuras de datos; la copia de grandes objetos de datos tiene un precio.

Pero los datos deben cambiar porque, de lo contrario, no hay necesidad de sitios o aplicaciones dinámicos. Lo importante es cómo se modifican los datos. La inmutabilidad proporciona la forma correcta de cambiar los datos (o el estado) de una aplicación. Esto hace posible rastrear los cambios de estado y determinar qué partes de la aplicación deberían volver a renderizarse como resultado de ese cambio.

Aprender sobre la inmutabilidad por primera vez será confuso. Pero mejorará a medida que se encuentre con errores que surgen cuando el estado cambia. A menudo, esa es la forma más clara de comprender la necesidad y los beneficios de la inmutabilidad.

Otras lecturas

  • Inmutabilidad en React y Redux
  • Immutablejs 101: mapas y listas
  • Usando Immutable.js con Redux