Ir al contenido principal

Datos inmutables

Traducción Beta No Oficial

Esta página fue traducida por PageTurner AI (beta). No está respaldada oficialmente por el proyecto. ¿Encontraste un error? Reportar problema →

Preguntas frecuentes de Redux: Datos inmutables

¿Cuáles son los beneficios de la inmutabilidad?

La inmutabilidad puede mejorar el rendimiento de tu aplicación y simplificar la programación y depuración, ya que los datos que no cambian son más fáciles de razonar que los datos que pueden modificarse arbitrariamente en cualquier parte de tu aplicación.

En particular, la inmutabilidad en aplicaciones web permite implementar técnicas sofisticadas de detección de cambios de forma simple y económica, asegurando que el proceso costoso de actualizar el DOM ocurra solo cuando sea estrictamente necesario (un pilar fundamental de las mejoras de rendimiento de React frente a otras bibliotecas).

Más información

Artículos

¿Por qué requiere Redux inmutabilidad?

Información adicional

Documentación

Debates

¿Por qué el uso de comprobación superficial de igualdad en Redux requiere inmutabilidad?

El uso de comprobación superficial de igualdad en Redux requiere inmutabilidad para que los componentes conectados se actualicen correctamente. Para entender por qué, debemos comprender la diferencia entre comprobación superficial y profunda de igualdad en JavaScript.

¿En qué se diferencian la comprobación superficial y profunda de igualdad?

La comprobación superficial de igualdad (o igualdad por referencia) simplemente verifica que dos variables diferentes referencien el mismo objeto; en contraste, la comprobación profunda de igualdad (o igualdad por valor) debe verificar cada valor de las propiedades de dos objetos.

Una comprobación superficial de igualdad es tan simple (y rápida) como a === b, mientras que una comprobación profunda implica un recorrido recursivo a través de las propiedades de dos objetos, comparando el valor de cada propiedad en cada paso.

Es por esta mejora de rendimiento que Redux utiliza comprobación superficial de igualdad.

Información adicional

Artículos

¿Cómo utiliza Redux la comprobación superficial de igualdad?

Redux utiliza la comprobación superficial de igualdad en su función combineReducers para devolver una nueva copia mutada del objeto de estado raíz o, si no se han realizado mutaciones, el objeto de estado raíz actual.

Información adicional

Documentación

¿Cómo utiliza combineReducers la comprobación superficial de igualdad?

La estructura recomendada para un almacén Redux es dividir el objeto de estado en múltiples "porciones" o "dominios" por clave, proporcionando una función reductora separada para gestionar cada porción de datos individual.

combineReducers facilita el trabajo con este estilo de estructura al tomar un argumento reducers definido como una tabla hash que contiene un conjunto de pares clave/valor, donde cada clave es el nombre de una porción de estado y el valor correspondiente es la función reductora que actuará sobre ella.

Por ejemplo, si la forma de tu estado es { todos, counter }, la llamada a combineReducers sería:

combineReducers({ todos: myTodosReducer, counter: myCounterReducer })

donde:

  • las claves todos y counter se refieren cada una a una porción de estado separada;

  • los valores myTodosReducer y myCounterReducer son funciones reductoras, actuando cada una sobre la porción de estado identificada por su clave respectiva.

combineReducers itera a través de cada uno de estos pares clave/valor. En cada iteración:

  • crea una referencia a la porción de estado actual identificada por cada clave;

  • llama al reductor apropiado pasándole esa porción;

  • crea una referencia a la porción de estado potencialmente mutada que devuelve el reductor.

Mientras avanza en las iteraciones, combineReducers construye un nuevo objeto de estado con las porciones devueltas por cada reductor. Este nuevo objeto puede ser diferente o no del estado actual. Aquí es donde combineReducers utiliza la comprobación superficial de igualdad para determinar si el estado ha cambiado.

Específicamente, en cada etapa de la iteración, combineReducers realiza una comprobación superficial de igualdad entre la porción de estado actual y la porción devuelta por el reductor. Si el reductor devuelve un nuevo objeto, la comprobación fallará y combineReducers establecerá una bandera hasChanged en true.

Tras completar las iteraciones, combineReducers comprueba el estado de la bandera hasChanged. Si es true, devuelve el nuevo objeto de estado construido. Si es false, devuelve el objeto de estado actual.

Es crucial destacar: Si todos los reductores devuelven el mismo objeto state que recibieron, entonces combineReducers devolverá el objeto de estado raíz actual, no el nuevo actualizado.

Información adicional

Documentación

Vídeo

¿Cómo utiliza React-Redux la comprobación superficial de igualdad?

React-Redux utiliza la comprobación superficial de igualdad para determinar si el componente que está envolviendo necesita volver a renderizarse.

Para ello, asume que el componente envuelto es puro; es decir, que producirá los mismos resultados dados los mismos props y estado.

Al asumir que el componente envuelto es puro, solo necesita comprobar si el objeto raíz del estado o los valores devueltos por mapStateToProps han cambiado. Si no lo han hecho, no es necesario volver a renderizar el componente envuelto.

Detecta un cambio manteniendo una referencia al objeto raíz del estado y una referencia a cada valor en el objeto de props devuelto por la función mapStateToProps.

Luego realiza una comprobación superficial de igualdad entre su referencia al objeto raíz del estado y el objeto de estado que recibe, y una serie de comprobaciones superficiales separadas entre cada referencia a los valores del objeto de props y los devueltos al ejecutar nuevamente mapStateToProps.

Información adicional

Documentación

Artículos

¿Por qué React-Redux comprueba superficialmente cada valor dentro del objeto de props devuelto por mapStateToProp?

React-Redux realiza una comprobación superficial de igualdad en cada valor dentro del objeto de props, no en el objeto de props en sí.

Lo hace porque el objeto de props es en realidad un hash de nombres de props y sus valores (o funciones selectoras utilizadas para recuperar o generar los valores), como en este ejemplo:

function mapStateToProps(state) {
return {
todos: state.todos, // prop value
visibleTodos: getVisibleTodos(state) // selector
}
}

export default connect(mapStateToProps)(TodoApp)

Por lo tanto, una comprobación superficial de igualdad en el objeto de props devuelto por llamadas repetidas a mapStateToProps siempre fallaría, ya que cada vez se devolvería un nuevo objeto.

React-Redux mantiene por separado referencias a cada valor en el objeto de props devuelto.

Información adicional

Artículos

¿Cómo utiliza React-Redux la comprobación superficial de igualdad para determinar si un componente necesita volver a renderizarse?

Cada vez que se llama a la función connect de React-Redux, realiza una comprobación superficial de igualdad entre su referencia almacenada al objeto raíz del estado y el objeto raíz del estado actual pasado desde el store. Si la comprobación es positiva, el objeto raíz del estado no se ha actualizado, por lo que no es necesario volver a renderizar el componente ni siquiera llamar a mapStateToProps.

Sin embargo, si la comprobación falla, el objeto raíz del estado ha sido actualizado, por lo que connect llamará a mapStateToProps para ver si las props del componente envuelto han cambiado.

Hace esto realizando una comprobación superficial de igualdad en cada valor dentro del objeto individualmente, y solo desencadenará un nuevo renderizado si una de esas comprobaciones falla.

En el ejemplo siguiente, si state.todos y el valor devuelto por getVisibleTodos() no cambian en llamadas sucesivas a connect, el componente no se volverá a renderizar.

function mapStateToProps(state) {
return {
todos: state.todos, // prop value
visibleTodos: getVisibleTodos(state) // selector
}
}

export default connect(mapStateToProps)(TodoApp)

Por el contrario, en este otro ejemplo (a continuación), el componente siempre se volverá a renderizar, ya que el valor de todos siempre es un nuevo objeto, independientemente de si sus valores cambian o no:

// AVOID - will always cause a re-render
function mapStateToProps(state) {
return {
// todos always references a newly-created object
todos: {
all: state.todos,
visibleTodos: getVisibleTodos(state)
}
}
}

export default connect(mapStateToProps)(TodoApp)

Si la comprobación superficial de igualdad falla entre los nuevos valores devueltos por mapStateToProps y los valores anteriores a los que React-Redux mantenía referencia, se desencadenará un nuevo renderizado del componente.

Información adicional

Artículos

Debates

¿Por qué la comprobación superficial de igualdad no funciona con objetos mutables?

La comprobación superficial de igualdad no puede detectar si una función muta un objeto que recibe cuando dicho objeto es mutable.

Esto ocurre porque dos variables que referencian el mismo objeto siempre serán iguales, independientemente de si los valores del objeto cambian o no, ya que ambas apuntan al mismo objeto. Por lo tanto, lo siguiente siempre devolverá verdadero:

function mutateObj(obj) {
obj.key = 'newValue'
return obj
}

const param = { key: 'originalValue' }
const returnVal = mutateObj(param)

param === returnVal
//> true

La comprobación superficial de param y returnValue simplemente verifica si ambas variables referencian el mismo objeto, lo cual es cierto. mutateObj() puede devolver una versión mutada de obj, pero sigue siendo el mismo objeto que se pasó. El hecho de que sus valores hayan cambiado dentro de mutateObj no afecta en absoluto a una comprobación superficial.

Información adicional

Artículos

¿La comprobación superficial con objetos mutables causa problemas en Redux?

La comprobación superficial con objetos mutables no causa problemas en Redux, pero sí causa problemas en bibliotecas que dependen del store, como React-Redux.

Concretamente, si el fragmento de estado que combineReducers pasa a un reducer es un objeto mutable, el reducer puede modificarlo directamente y devolverlo.

Si lo hace, la comprobación superficial de igualdad que realiza combineReducers siempre será positiva, ya que los valores del fragmento de estado pueden haber mutado, pero el objeto en sí no ha cambiado: sigue siendo el mismo que se pasó al reducer.

En consecuencia, combineReducers no activará su bandera hasChanged, aunque el estado haya cambiado. Si ningún otro reducer devuelve un nuevo fragmento de estado actualizado, la bandera hasChanged permanecerá en falso, haciendo que combineReducers devuelva el objeto raíz del estado existente.

El store se actualizará con los nuevos valores del estado raíz, pero como el objeto raíz del estado sigue siendo el mismo, bibliotecas vinculadas a Redux como React-Redux no detectarán la mutación del estado y, por tanto, no activarán el rerenderizado de los componentes conectados.

Información adicional

Documentación

¿Por qué un reducer que muta el estado impide que React-Redux rerenderice componentes?

Si un reducer de Redux muta directamente y devuelve el objeto de estado que recibe, los valores del objeto raíz del estado cambiarán, pero el objeto en sí permanecerá igual.

Dado que React-Redux realiza una comprobación superficial del objeto de estado raíz para determinar si sus componentes envueltos necesitan volver a renderizarse, no podrá detectar la mutación del estado y, por lo tanto, no activará un nuevo renderizado.

Información adicional

Documentación

¿Por qué un selector que muta y devuelve un objeto persistente a mapStateToProps evita que React-Redux vuelva a renderizar un componente envuelto?

Si uno de los valores del objeto de props devuelto por mapStateToProps es un objeto que persiste entre llamadas a connect (como, potencialmente, el objeto de estado raíz), pero es mutado y devuelto directamente por una función selector, React-Redux no podrá detectar la mutación y, por lo tanto, no activará un nuevo renderizado del componente envuelto.

Como hemos visto, los valores del objeto mutable devuelto por la función selector pueden haber cambiado, pero el objeto en sí no, y la comprobación superficial de igualdad solo compara los objetos mismos, no sus valores.

Por ejemplo, la siguiente función mapStateToProps nunca activará un nuevo renderizado:

// State object held in the Redux store
const state = {
user: {
accessCount: 0,
name: 'keith'
}
}

// Selector function
const getUser = state => {
++state.user.accessCount // mutate the state object
return state
}

// mapStateToProps
const mapStateToProps = state => ({
// The object returned from getUser() is always
// the same object, so this wrapped
// component will never re-render, even though it's been
// mutated
userRecord: getUser(state)
})

const a = mapStateToProps(state)
const b = mapStateToProps(state)

a.userRecord === b.userRecord
//> true

Ten en cuenta que, a la inversa, si se usa un objeto inmutable, el componente podría volver a renderizarse cuando no debería.

Información adicional

Artículos

Debates

¿Cómo permite la inmutabilidad que una comprobación superficial detecte mutaciones en objetos?

Si un objeto es inmutable, cualquier cambio que deba hacerse en él dentro de una función debe realizarse en una copia del objeto.

Esta copia mutada es un objeto separado del que se pasó a la función, por lo que cuando se devuelve, una comprobación superficial identificará que es un objeto diferente al que se pasó originalmente, y por lo tanto fallará.

Información adicional

Artículos

¿Cómo puede la inmutabilidad en tus reductores causar renderizados innecesarios en componentes?

No puedes mutar un objeto inmutable; en su lugar, debes mutar una copia del mismo, dejando el original intacto.

Esto está perfectamente bien cuando mutas la copia, pero en el contexto de un reductor, si devuelves una copia que no ha sido mutada, la función combineReducers de Redux aún pensará que el estado necesita actualizarse, ya que estás devolviendo un objeto completamente diferente al objeto de porción de estado que se pasó originalmente.

combineReducers devolverá entonces este nuevo objeto de estado raíz al store. El nuevo objeto tendrá los mismos valores que el objeto de estado raíz actual, pero al ser un objeto diferente, hará que el store se actualice, lo que finalmente causará que todos los componentes conectados se vuelvan a renderizar innecesariamente.

Para evitar que esto suceda, debes devolver siempre el objeto de porción de estado que se pasó a un reductor si este no muta el estado.

Información adicional

Artículos

¿Cómo puede causar la inmutabilidad en mapStateToProps renderizados innecesarios?

Ciertas operaciones inmutables, como un filtro de Array, siempre devolverán un nuevo objeto, incluso si los valores en sí no han cambiado.

Si esta operación se usa como función selector en mapStateToProps, la comprobación superficial de igualdad que React-Redux realiza en cada valor del objeto de props devuelto siempre fallará, ya que el selector retorna un nuevo objeto cada vez.

Por tanto, aunque los valores de ese nuevo objeto no hayan cambiado, el componente envuelto siempre se volverá a renderizar.

Por ejemplo, lo siguiente siempre desencadenará un re-renderizado:

// A JavaScript array's 'filter' method treats the array as immutable,
// and returns a filtered copy of the array.
const getVisibleTodos = todos => todos.filter(t => !t.completed)

const state = {
todos: [
{
text: 'do todo 1',
completed: false
},
{
text: 'do todo 2',
completed: true
}
]
}

const mapStateToProps = state => ({
// getVisibleTodos() always returns a new array, and so the
// 'visibleToDos' prop will always reference a different array,
// causing the wrapped component to re-render, even if the array's
// values haven't changed
visibleToDos: getVisibleTodos(state.todos)
})

const a = mapStateToProps(state)
// Call mapStateToProps(state) again with exactly the same arguments
const b = mapStateToProps(state)

a.visibleToDos
//> { "completed": false, "text": "do todo 1" }

b.visibleToDos
//> { "completed": false, "text": "do todo 1" }

a.visibleToDos === b.visibleToDos
//> false

Nota que, a la inversa, si los valores en tu objeto de props hacen referencia a objetos mutables, tu componente podría no renderizarse cuando debería.

Información adicional

Artículos

¿Qué enfoques existen para manejar la inmutabilidad de datos? ¿Debo usar Immer?

No necesitas usar Immer con Redux. JavaScript plano, si se escribe correctamente, es perfectamente capaz de proporcionar inmutabilidad sin necesidad de usar una biblioteca especializada.

Sin embargo, garantizar la inmutabilidad con JavaScript es difícil, y es fácil mutar un objeto accidentalmente, causando errores en tu aplicación que son extremadamente difíciles de localizar. Por esta razón, usar una biblioteca de utilidades para actualizaciones inmutables como Immer puede mejorar significativamente la fiabilidad de tu aplicación y facilitar mucho su desarrollo.

Información adicional

Debates

¿Cuáles son los problemas de usar JavaScript plano para operaciones inmutables?

JavaScript nunca fue diseñado para proporcionar operaciones inmutables garantizadas. Por ello, hay varios problemas que debes tener en cuenta si decides usarlo para operaciones inmutables en tu aplicación Redux.

Mutación accidental de objetos

Con JavaScript, puedes mutar accidentalmente un objeto (como el árbol de estado de Redux) con facilidad sin darte cuenta. Por ejemplo, actualizar propiedades anidadas profundamente, crear una nueva referencia a un objeto en lugar de un nuevo objeto, o realizar una copia superficial en vez de una copia profunda, pueden llevar a mutaciones de objetos inadvertidas que pueden confundir incluso a programadores JavaScript experimentados.

Para evitar estos problemas, asegúrate de seguir los patrones recomendados de actualización inmutable.

Código verboso

Actualizar árboles de estado anidados complejos puede generar código verboso tedioso de escribir y difícil de depurar.

Bajo rendimiento

Operar con objetos y arrays de JavaScript de forma inmutable puede ser lento, particularmente cuando tu árbol de estado crece.

Recuerda que para modificar un objeto inmutable debes mutar una copia del mismo, y copiar objetos grandes puede ser lento ya que cada propiedad debe ser duplicada.

En contraste, bibliotecas de inmutabilidad como Immer pueden emplear structural sharing (compartición estructural), que devuelve eficientemente un nuevo objeto reutilizando gran parte del objeto existente que se está copiando.

Información adicional

Documentación

Artículos