Ir al contenido principal
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 →

Patrones de Actualización Inmutables

Los artículos listados en Conceptos Previos#Gestión de Datos Inmutables ofrecen varios buenos ejemplos de cómo realizar operaciones básicas de actualización de forma inmutable, como actualizar un campo en un objeto o añadir un elemento al final de un array. Sin embargo, los reductores a menudo necesitarán combinar estas operaciones básicas para realizar tareas más complejas. Aquí presentamos ejemplos de algunas tareas comunes que podrías necesitar implementar.

Actualización de Objetos Anidados

La clave para actualizar datos anidados es que cada nivel de anidamiento debe copiarse y actualizarse adecuadamente. Este concepto suele ser difícil para quienes aprenden Redux, y existen problemas específicos que ocurren frecuentemente al intentar actualizar objetos anidados. Estos conducen a mutaciones directas accidentales y deben evitarse.

Enfoque Correcto: Copiar Todos los Niveles de Datos Anidados

Desafortunadamente, el proceso de aplicar actualizaciones inmutables correctamente a estados profundamente anidados puede volverse fácilmente verboso y difícil de leer. Así es como podría verse un ejemplo de actualización de state.first.second[someId].fourth:

function updateVeryNestedField(state, action) {
return {
...state,
first: {
...state.first,
second: {
...state.first.second,
[action.someId]: {
...state.first.second[action.someId],
fourth: action.someValue
}
}
}
}
}

Obviamente, cada capa de anidamiento dificulta la lectura y aumenta las posibilidades de errores. Esta es una de las razones por las que se recomienda mantener tu estado lo más plano posible y componer reductores tanto como sea factible.

Error Común Nº1: Nuevas variables que apuntan a los mismos objetos

Definir una nueva variable no crea un nuevo objeto real, solo crea otra referencia al mismo objeto. Un ejemplo de este error sería:

function updateNestedState(state, action) {
let nestedState = state.nestedState
// ERROR: this directly modifies the existing object reference - don't do this!
nestedState.nestedField = action.data

return {
...state,
nestedState
}
}

Esta función sí devuelve correctamente una copia superficial del objeto de estado de nivel superior, pero como la variable nestedState seguía apuntando al objeto existente, el estado se mutó directamente.

Error Común Nº2: Hacer solo una copia superficial de un nivel

Otra versión común de este error luce así:

function updateNestedState(state, action) {
// Problem: this only does a shallow copy!
let newState = { ...state }

// ERROR: nestedState is still the same object!
newState.nestedState.nestedField = action.data

return newState
}

Hacer una copia superficial del nivel superior no es suficiente; el objeto nestedState también debe copiarse.

Inserción y Eliminación de Elementos en Arrays

Normalmente, el contenido de un array en JavaScript se modifica usando funciones mutables como push, unshift y splice. Como no queremos mutar el estado directamente en los reductores, estas deben evitarse. Por ello, es posible que veas comportamientos de "inserción" o "eliminación" escritos así:

function insertItem(array, action) {
return [
...array.slice(0, action.index),
action.item,
...array.slice(action.index)
]
}

function removeItem(array, action) {
return [...array.slice(0, action.index), ...array.slice(action.index + 1)]
}

Sin embargo, recuerda que lo crucial es que no se modifique la referencia original en memoria. Mientras hagamos primero una copia, podemos mutar esa copia con seguridad. Esto aplica tanto para arrays como objetos, pero los valores anidados aún deben actualizarse siguiendo las mismas reglas.

Esto significa que también podríamos escribir las funciones de inserción y eliminación así:

function insertItem(array, action) {
let newArray = array.slice()
newArray.splice(action.index, 0, action.item)
return newArray
}

function removeItem(array, action) {
let newArray = array.slice()
newArray.splice(action.index, 1)
return newArray
}

La función de eliminación también podría implementarse como:

function removeItem(array, action) {
return array.filter((item, index) => index !== action.index)
}

Actualización de un Elemento en un Array

Actualizar un elemento en un array puede lograrse usando Array.map, devolviendo un nuevo valor para el elemento que queremos actualizar y manteniendo los valores existentes para los demás:

function updateObjectInArray(array, action) {
return array.map((item, index) => {
if (index !== action.index) {
// This isn't the item we care about - keep it as-is
return item
}

// Otherwise, this is the one we want - return an updated value
return {
...item,
...action.item
}
})
}

Bibliotecas de Utilidades para Actualizaciones Inmutables

Dado que escribir código de actualización inmutable puede volverse tedioso, existen varias bibliotecas de utilidades que intentan abstraer este proceso. Estas bibliotecas varían en APIs y uso, pero todas buscan proporcionar una forma más breve y concisa de escribir estas actualizaciones. Por ejemplo, Immer simplifica las actualizaciones inmutables mediante funciones y objetos JavaScript planos:

var usersState = [{ name: 'John Doe', address: { city: 'London' } }]
var newState = immer.produce(usersState, draftState => {
draftState[0].name = 'Jon Doe'
draftState[0].address.city = 'Paris'
//nested update similar to mutable way
})

Algunas, como dot-prop-immutable, utilizan rutas de texto para los comandos:

state = dotProp.set(state, `todos.${index}.complete`, true)

Otras, como immutability-helper (un fork del complemento React Immutability Helpers ahora obsoleto), utilizan valores anidados y funciones auxiliares:

var collection = [1, 2, { a: [12, 17, 15] }]
var newCollection = update(collection, {
2: { a: { $splice: [[1, 1, 13, 14]] } }
})

Pueden ofrecer una alternativa útil para escribir lógica de actualización inmutable manual.

Puedes encontrar una lista de muchas utilidades de actualización inmutable en la sección Immutable Data#Immutable Update Utilities del Catálogo de complementos Redux.

Simplificación de actualizaciones inmutables con Redux Toolkit

Nuestro paquete Redux Toolkit incluye una utilidad createReducer que internamente usa Immer. Gracias a esto, puedes escribir reductores que aparentemente "mutan" el estado, pero las actualizaciones se aplican de forma inmutable.

Esto permite escribir la lógica de actualización inmutable de forma mucho más simple. Así podría verse el ejemplo de datos anidados usando createReducer:

import { createReducer } from '@reduxjs/toolkit'

const initialState = {
first: {
second: {
id1: { fourth: 'a' },
id2: { fourth: 'b' }
}
}
}

const reducer = createReducer(initialState, {
UPDATE_ITEM: (state, action) => {
state.first.second[action.someId].fourth = action.someValue
}
})

Claramente es mucho más corto y fácil de leer. Sin embargo, esto solo funciona correctamente si usas la función "mágica" createReducer de Redux Toolkit que envuelve este reductor en la función produce de Immer. Si este reductor se usa sin Immer, ¡realmente mutará el estado!. Tampoco es evidente solo con mirar el código que esta función es segura y actualiza el estado inmutalmente. Por favor asegúrate de entender completamente los conceptos de actualizaciones inmutables. Si decides usar esto, puede ayudar añadir comentarios en tu código que expliquen que tus reductores usan Redux Toolkit e Immer.

Además, la utilidad createSlice de Redux Toolkit generará automáticamente creadores de acciones y tipos de acción basados en las funciones reductoras que proporciones, con las mismas capacidades de actualización potenciadas por Immer.

Más información