React, Redux e Immutable.js: ingredientes para aplicaciones web eficientes
Publicado: 2022-03-11React, Redux e Immutable.js se encuentran actualmente entre las bibliotecas de JavaScript más populares y se están convirtiendo rápidamente en la primera opción de los desarrolladores cuando se trata de desarrollo front-end. En los pocos proyectos de React y Redux en los que he trabajado, me di cuenta de que muchos desarrolladores que comienzan con React no entienden completamente React y cómo escribir código eficiente para utilizar todo su potencial.
En este tutorial de Immutable.js, crearemos una aplicación simple usando React y Redux, e identificaremos algunos de los usos indebidos más comunes de React y formas de evitarlos.
Problema de referencia de datos
React tiene que ver con el rendimiento. Fue construido desde cero para tener un rendimiento extremo, solo volviendo a renderizar partes mínimas de DOM para satisfacer los nuevos cambios de datos. Cualquier aplicación React debe consistir principalmente en pequeños componentes simples (o funciones sin estado). Son fáciles de razonar y la mayoría de ellos pueden tener la función shouldComponentUpdate devolviendo false .
shouldComponentUpdate(nextProps, nextState) { return false; }En cuanto al rendimiento, la función de ciclo de vida del componente más importante es shouldComponentUpdate y, si es posible, siempre debería devolver false . Esto asegura que este componente nunca se vuelva a renderizar (excepto el renderizado inicial) haciendo que la aplicación React se sienta extremadamente rápida.
Cuando ese no es el caso, nuestro objetivo es hacer una comprobación de igualdad económica de accesorios/estado antiguos frente a accesorios/estado nuevos y omitir la nueva representación si los datos no se modifican.
Demos un paso atrás por un segundo y revisemos cómo JavaScript realiza comprobaciones de igualdad para diferentes tipos de datos.
La verificación de igualdad para tipos de datos primitivos como boolean , string y integer es muy simple ya que siempre se comparan por su valor real:
1 === 1 'string' === 'string' true === truePor otro lado, la verificación de igualdad para tipos complejos como objetos , matrices y funciones es completamente diferente. Dos objetos son iguales si tienen la misma referencia (apuntando al mismo objeto en la memoria).
const obj1 = { prop: 'someValue' }; const obj2 = { prop: 'someValue' }; console.log(obj1 === obj2); // falseAunque obj1 y obj2 parecen ser iguales, su referencia es diferente. Dado que son diferentes, compararlos ingenuamente dentro de la función shouldComponentUpdate hará que nuestro componente se vuelva a renderizar innecesariamente.
Lo importante a tener en cuenta es que los datos provenientes de los reductores de Redux, si no se configuran correctamente, siempre se servirán con una referencia diferente, lo que hará que el componente se vuelva a procesar cada vez.
Este es un problema central en nuestra búsqueda para evitar la renderización de componentes.
Manejo de referencias
Tomemos un ejemplo en el que tenemos objetos profundamente anidados y queremos compararlo con su versión anterior. Podríamos recorrer recursivamente los accesorios de objetos anidados y comparar cada uno, pero obviamente eso sería extremadamente costoso y está fuera de discusión.
Eso nos deja con una sola solución, y es verificar la referencia, pero surgen nuevos problemas rápidamente:
- Preservar la referencia si nada ha cambiado
- Cambio de referencia si alguno de los valores de propiedad de matriz/objeto anidado cambió
Esta no es una tarea fácil si queremos hacerlo de una manera agradable, limpia y optimizada para el rendimiento. Facebook se dio cuenta de este problema hace mucho tiempo y llamó a Immutable.js al rescate.
import { Map } from 'immutable'; // transform object into immutable map let obj1 = Map({ prop: 'someValue' }); const obj2 = obj1; console.log(obj1 === obj2); // true obj1 = obj1.set('prop', 'someValue'); // set same old value console.log(obj1 === obj2); // true | does not break reference because nothing has changed obj1 = obj1.set('prop', 'someNewValue'); // set new value console.log(obj1 === obj2); // false | breaks reference Ninguna de las funciones de Immutable.js realiza una mutación directa en los datos proporcionados. En cambio, los datos se clonan internamente, se mutan y, si hubo algún cambio, se devuelve una nueva referencia. De lo contrario, devuelve la referencia inicial. La nueva referencia debe establecerse explícitamente, como obj1 = obj1.set(...); .
Ejemplos de React, Redux e Immutable.js
La mejor manera de demostrar el poder de estas bibliotecas es crear una aplicación sencilla. ¿Y qué puede ser más simple que una aplicación de tareas pendientes?
Para abreviar, en este artículo solo analizaremos las partes de la aplicación que son fundamentales para estos conceptos. El código fuente completo del código de la aplicación se puede encontrar en GitHub.
Cuando se inicie la aplicación, notará que las llamadas a console.log se colocan convenientemente en áreas clave para mostrar claramente la cantidad de DOM que se vuelve a procesar, que es mínima.
Como cualquier otra aplicación de tareas pendientes, queremos mostrar una lista de tareas pendientes. Cuando el usuario haga clic en un elemento pendiente, lo marcaremos como completado. También necesitamos un pequeño campo de entrada en la parte superior para agregar nuevos todos y en la parte inferior 3 filtros que permitirán al usuario alternar entre:
- Todos
- Terminado
- Activo
Reductor de reducción
Todos los datos en la aplicación Redux viven dentro de un solo objeto de tienda y podemos ver los reductores como una forma conveniente de dividir la tienda en partes más pequeñas que son más fáciles de razonar. Dado que el reductor también es una función, también se puede dividir en partes aún más pequeñas.
Nuestro reductor constará de 2 piezas pequeñas:
- lista de quehaceres
- filtro activo
// reducers/todos.js import * as types from 'constants/ActionTypes'; // we can look at List/Map as immutable representation of JS Array/Object import { List, Map } from 'immutable'; import { combineReducers } from 'redux'; function todoList(state = List(), action) { // default state is empty List() switch (action.type) { case types.ADD_TODO: return state.push(Map({ // Every switch/case must always return either immutable id: action.id, // or primitive (like in activeFilter) state data text: action.text, // We let Immutable decide if data has changed or not isCompleted: false, })); // other cases... default: return state; } } function activeFilter(state = 'all', action) { switch (action.type) { case types.CHANGE_FILTER: return action.filter; // This is primitive data so there's no need to worry default: return state; } } // combineReducers combines reducers into a single object // it lets us create any number or combination of reducers to fit our case export default combineReducers({ activeFilter, todoList, });Conexión con Redux
Ahora que hemos configurado un reductor Redux con datos Immutable.js, conectémoslo con el componente React para pasar los datos.
// components/App.js import { connect } from 'react-redux'; // ….component code const mapStateToProps = state => ({ activeFilter: state.todos.activeFilter, todoList: state.todos.todoList, }); export default connect(mapStateToProps)(App);En un mundo perfecto, la conexión debe realizarse solo en los componentes de ruta de nivel superior, extrayendo los datos en mapStateToProps y el resto son accesorios básicos de React para pasar a los niños. En aplicaciones a gran escala, tiende a ser difícil realizar un seguimiento de todas las conexiones, por lo que queremos mantenerlas al mínimo.
Es muy importante tener en cuenta que state.todos es un objeto JavaScript normal devuelto por la función combineReducers de Redux (todos es el nombre del reductor), pero state.todos.todoList es una lista inmutable y es fundamental que permanezca en tal estado. formulario hasta que pase la verificación shouldComponentUpdate .
Evitar el renderizado de componentes
Antes de profundizar, es importante comprender qué tipo de datos se deben entregar al componente:
- Tipos primitivos de cualquier tipo.
- Objeto/matriz solo en forma inmutable
Tener este tipo de datos nos permite comparar superficialmente los accesorios que entran en los componentes de React.
El siguiente ejemplo muestra cómo diferenciar los accesorios de la forma más sencilla posible:
$ npm install react-pure-render import shallowEqual from 'react-pure-render/shallowEqual'; shouldComponentUpdate(nextProps, nextState) { return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState); }La función "shallowEqual" comprobará las diferencias de props/state solo a 1 nivel de profundidad. Funciona extremadamente rápido y está en perfecta sinergia con nuestros datos inmutables. Tener que escribir este shouldComponentUpdate en cada componente sería muy inconveniente, pero afortunadamente hay una solución simple.

Extraiga shouldComponentUpdate en un componente separado especial:
// components/PureComponent.js import React from 'react'; import shallowEqual from 'react-pure-render/shallowEqual'; export default class PureComponent extends React.Component { shouldComponentUpdate(nextProps, nextState) { return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState); } }Luego, simplemente extienda cualquier componente en el que se desee esta lógica shouldComponentUpdate:
// components/Todo.js export default class Todo extends PureComponent { // Component code }Esta es una forma muy limpia y eficiente de evitar que los componentes se vuelvan a renderizar en la mayoría de los casos, y luego, si la aplicación se vuelve más compleja y de repente requiere una solución personalizada, se puede cambiar fácilmente.
Hay un pequeño problema al usar PureComponent al pasar funciones como accesorios. Dado que React, con la clase ES6, no vincula automáticamente esto a las funciones, tenemos que hacerlo manualmente. Podemos lograr esto haciendo uno de los siguientes:
- use el enlace de la función de flecha ES6:
<Component onClick={() => this.handleClick()} /> - use bind :
<Component onClick={this.handleClick.bind(this)} />
Ambos enfoques harán que Componente se vuelva a procesar porque se ha pasado una referencia diferente a onClick cada vez.
Para solucionar este problema, podemos vincular funciones previamente en el método constructor de la siguiente manera:
constructor() { super(); this.handleClick = this.handleClick.bind(this); } // Then simply pass the function render() { return <Component onClick={this.handleClick} /> }Si se encuentra previnculando varias funciones la mayor parte del tiempo, podemos exportar y reutilizar una pequeña función auxiliar:
// utils/bind-functions.js export default function bindFunctions(functions) { functions.forEach(f => this[f] = this[f].bind(this)); } // some component constructor() { super(); bindFunctions.call(this, ['handleClick']); // Second argument is array of function names }Si ninguna de las soluciones funciona para usted, siempre puede escribir las condiciones shouldComponentUpdate manualmente.
Manejo de datos inmutables dentro de un componente
Con la configuración actual de datos inmutables, se ha evitado volver a renderizar y nos quedan datos inmutables dentro de los accesorios de un componente. Hay varias formas de usar estos datos inmutables, pero el error más común es convertir los datos de inmediato en JS simple usando la función toJS inmutable.
El uso de toJS para convertir profundamente los datos inmutables en JS simple anula todo el propósito de evitar volver a renderizar porque, como era de esperar, es muy lento y, como tal, debe evitarse. Entonces, ¿cómo manejamos los datos inmutables?
Debe usarse tal cual, es por eso que la API inmutable proporciona una amplia variedad de funciones, mapas y se usa más comúnmente dentro del componente React. La estructura de datos todoList que proviene de Redux Reducer es una matriz de objetos en forma inmutable, cada objeto representa un solo elemento de tarea:
[{ id: 1, text: 'todo1', isCompleted: false, }, { id: 2, text: 'todo2', isCompleted: false, }]La API Immutable.js es muy similar a JavaScript normal, por lo que usaríamos todoList como cualquier otra matriz de objetos. La función de mapa resulta mejor en la mayoría de los casos.
Dentro de una devolución de llamada de mapa obtenemos todo , que es un objeto que aún está en forma inmutable y podemos pasarlo de manera segura en el componente Todo .
// components/TodoList.js render() { return ( // …. {todoList.map(todo => { return ( <Todo key={todo.get('id')} todo={todo}/> ); })} // …. ); }Si planea realizar múltiples iteraciones encadenadas sobre datos inmutables como:
myMap.filter(somePred).sort(someComp)… entonces es muy importante convertirlo primero en Seq usando toSeq y después de las iteraciones volverlo a la forma deseada como:
myMap.toSeq().filter(somePred).sort(someComp).toOrderedMap()Dado que Immutable.js nunca muta directamente los datos dados, siempre necesita hacer otra copia de ellos, realizar múltiples iteraciones como esta puede ser muy costoso. Seq es una secuencia de datos inmutable y perezosa, lo que significa que realizará la menor cantidad de operaciones posible para realizar su tarea y omitirá la creación de copias intermedias. Seq fue construido para ser utilizado de esta manera.
Dentro del componente Todo use get o getIn para obtener los accesorios.
Bastante simple, ¿verdad?
Bueno, me di cuenta de que muchas veces puede volverse muy ilegible tener una gran cantidad de get() y especialmente getIn() . Así que decidí encontrar un punto óptimo entre el rendimiento y la legibilidad y, después de algunos experimentos simples, descubrí que las funciones toObject y toArray de Immutable.js funcionan muy bien.
Estas funciones convierten superficialmente (1 nivel de profundidad) objetos/matrices Immutable.js en objetos/matrices de JavaScript sin formato. Si tenemos datos profundamente anidados en el interior, permanecerán en forma inmutable, listos para pasar a
Es más lento que get() solo por un margen insignificante, pero se ve mucho más limpio:
// components/Todo.js render() { const { id, text, isCompleted } = this.props.todo.toObject(); // ….. }Veámoslo todo en acción
En caso de que aún no hayas clonado el código de GitHub, ahora es un buen momento para hacerlo:
git clone https://github.com/rogic89/ToDo-react-redux-immutable.git cd ToDo-react-redux-immutableIniciar el servidor es tan simple (asegúrese de que Node.js y NPM estén instalados) como esto:
npm install npm start Navegue a http://localhost:3000 en su navegador web. Con la consola del desarrollador abierta, observe los registros a medida que agrega algunos elementos pendientes, márquelos como hechos y cambie el filtro:
- Agregar 5 elementos pendientes
- Cambie el filtro de 'Todos' a 'Activo' y luego vuelva a 'Todos'
- No hay que volver a renderizar, solo cambiar el filtro
- Marcar 2 elementos pendientes como completados
- Se volvieron a renderizar dos todos, pero solo uno a la vez.
- Cambie el filtro de 'Todos' a 'Activo' y luego vuelva a 'Todos'
- Solo se montaron/desmontaron 2 elementos pendientes completados
- Los activos no se volvieron a renderizar
- Eliminar un solo elemento de tareas pendientes del medio de la lista
- Solo se afectó el elemento de tareas pendientes eliminado, otros no se volvieron a procesar
Envolver
La sinergia de React, Redux e Immutable.js, cuando se usa correctamente, ofrece algunas soluciones elegantes para muchos problemas de rendimiento que a menudo se encuentran en aplicaciones web grandes.
Immutable.js nos permite detectar cambios en objetos/matrices de JavaScript sin recurrir a las ineficiencias de las comprobaciones de igualdad profundas, lo que a su vez permite a React evitar costosas operaciones de renderización cuando no son necesarias. Esto significa que el rendimiento de Immutable.js tiende a ser bueno en la mayoría de los escenarios.
Espero que te haya gustado el artículo y lo encuentres útil para construir soluciones innovadoras de React en tus proyectos futuros.
