Actualizando arreglos en el estado
Los arreglos son mutables en JavaScript, pero tú podrías tratarlos como inmutables cuando los almacenas en el estado. Justo como los objetos, cuando quieras actualizar un arreglo almacenado en el estado, necesitas crear uno nuevo ( o hacer una copia de uno existente), luego almacenarlo en el estado para hacer uso de este nuevo arreglo.
Aprenderás
- Cómo añadir, remover, o cambiar items en un arreglo en el estado de React
- Cómo actualizar un objeto dentro de un arreglo
- Cómo copiar un arreglo de forma menos repetitiva con Immer
Actualizando arreglos sin mutación
En JavaScript, los arreglos son sólo otro tipo de objeto. como con los objetos, deberías tratar los arreglos en estado React como de solo lectura. Esto significa que no deberías reasignar elementos dentro de un arreglo como arr[0] = 'pájaro'
, y tampoco deberías usar métodos que puedan mutar el arreglo, como push()
y pop()
.
En su lugar, cada vez que quieras actualizar un arreglo, querrás pasar un, nuevo arreglo a su función de configuración de estado.Para hacer eso, puedes crear un nuevo arreglo a partir de el arreglo original en su estado llamando a sus métodos no mutantes como filter()
por map()
. Luego puede establecer su estado a partir de un nuevo arreglo.
Aquí hay una tabla de referencia con las operaciones más comunes con arreglos. Cuando se trata de arreglos dentro de el estado de React, necesitarás evitar los métodos de la columna izquierda, y en su lugar es preferible usar los métodos de la columna derecha.
evita (muta el arreglo) | preferido (retorna un nuevo arreglo) | |
---|---|---|
añadiendo | push , unshift | concat , [...arr] operador de propagación (ejemplo) |
removiendo | pop , shift , splice | filter , slice (ejemplo) |
reemplazando | splice , arr[i] = ... asigna | map (ejemplo) |
ordenando | reverse , sort | copia el arreglo primero (ejemplo) |
Alternativamente, tú puedes usar Immer el cual te permite usar métodos de ambas columnas.
Añadiendo un arreglo
push()
muta un arreglo, lo cual no queremos:
import { useState } from 'react'; let nextId = 0; export default function List() { const [name, setName] = useState(''); const [artists, setArtists] = useState([]); return ( <> <h1>Escultores inspiradores:</h1> <input value={name} onChange={e => setName(e.target.value)} /> <button onClick={() => { setName(''); artists.push({ id: nextId++, name: name, }); }}>Añadir</button> <ul> {artists.map(artist => ( <li key={artist.id}>{artist.name}</li> ))} </ul> </> ); }
En su lugar, crea un nuevo arreglo el cual contenga los elementos existentes y un nuevo elemento al final. Hay multiples formas para hacer esto, pero la más fácil es usar la sintaxis ...
operador de propagación en arreglos:
setArtists( // Remplaza el estado
[ // con el nuevo arreglo
...artists, // el cual contiene todos los elementos antiguos
{ id: nextId++, name: name } // y un nuevo elemento al final
]
);
Ahora funciona correctamente:
import { useState } from 'react'; let nextId = 0; export default function List() { const [name, setName] = useState(''); const [artists, setArtists] = useState([]); return ( <> <h1>Escultores inspiradores:</h1> <input value={name} onChange={e => setName(e.target.value)} /> <button onClick={() => { setName(''); setArtists([ ...artists, { id: nextId++, name: name } ]); }}>Añadir</button> <ul> {artists.map(artist => ( <li key={artist.id}>{artist.name}</li> ))} </ul> </> ); }
El operador de propagación también te permite anteponer un elemento colocandolo antes de el original ...artists
:
setArtists([
{ id: nextId++, name: name },
...artists // Coloca los elementos antiguos al final
]);
De esta forma, el operador de propagación puede hacer el trabajo tanto de push()
añadiendo en el final del arreglo como de unshift()
agregando al comienzo de el arreglo. ¡Pruebalo en el editor de arriba!
Eliminando elementos de un arreglo
La forma más fácil de remover un elemento de un arreglo es filtrarlo. En otras palabras, producirás un nuevo arreglo el cual no contendrá ese elemento. Para hacer esto, usa el método filter
, por ejemplo:
import { useState } from 'react'; let initialArtists = [ { id: 0, name: 'Marta Colvin Andrade' }, { id: 1, name: 'Lamidi Olonade Fakeye'}, { id: 2, name: 'Louise Nevelson'}, ]; export default function List() { const [artists, setArtists] = useState( initialArtists ); return ( <> <h1>Escultores inspiradores:</h1> <ul> {artists.map(artist => ( <li key={artist.id}> {artist.name}{' '} <button onClick={() => { setArtists( artists.filter(a => a.id !== artist.id ) ); }}> Eliminar </button> </li> ))} </ul> </> ); }
Haz click en el botón de “Eliminar” varias veces, y mira su controlador de clics.
setArtists(
artists.filter(a => a.id !== artist.id)
);
Aquí, artists.filter(a => a.id !== artist.id)
significa “crea un nuevo arreglo el cual consista de aquellos artists
cuyos IDs son diferentes de artist.id
”. En otras palabras, el botón “Eliminar” de cada artista filtrará a ese artista de la matriz y luego solicitará una nueva representación con la matriz resultante. Ten en cuenta que filter
no modifica el array original.
Transformando un arreglo
Si deseas cambiar algunos o todos los elementos de el arreglo, puedes usar map()
para crear un nuevo arreglo. La función que pasará a map
puede decidir qué hacer con cada elemento, en función de sus datos o su índice (o ambos).
En este ejemplo, un arreglo contiene las coordenadas de dos círculos y un cuadrado. Cuando presiona el botón, mueve solo los círculos 50 píxeles hacia abajo. Lo hace produciendo un nuevo arreglo de datos usando map()
:
import { useState } from 'react'; let initialShapes = [ { id: 0, type: 'circle', x: 50, y: 100 }, { id: 1, type: 'square', x: 150, y: 100 }, { id: 2, type: 'circle', x: 250, y: 100 }, ]; export default function ShapeEditor() { const [shapes, setShapes] = useState( initialShapes ); function handleClick() { const nextShapes = shapes.map(shape => { if (shape.type === 'square') { // No cambia return shape; } else { // Devuelve un nuevo círculo 50px abajo return { ...shape, y: shape.y + 50, }; } }); // Vuelve a renderizar con la nueva matriz setShapes(nextShapes); } return ( <> <button onClick={handleClick}> ¡Mueve los círculos hacia abajo! </button> {shapes.map(shape => ( <div key={shape.id} style={{ background: 'purple', position: 'absolute', left: shape.x, top: shape.y, borderRadius: shape.type === 'circle' ? '50%' : '', width: 20, height: 20, }} /> ))} </> ); }
Reemplazo de elementos en un arreglo
Es particularmente común querer reemplazar uno o más elementos en una matriz. Las asignaciones como arr[0] = 'bird'
están mutando el arreglo original, por lo que también querrás usar map
para esto.
Para reemplazar un elemento, crea una un nuevo arreglo con map
. Dentro de su llamada map
, recibirá el índice del elemento como segundo argumento. Úsalo para decidir si devolver el elemento original (el primer argumento) o algo más:
import { useState } from 'react'; let initialCounters = [ 0, 0, 0 ]; export default function CounterList() { const [counters, setCounters] = useState( initialCounters ); function handleIncrementClick(index) { const nextCounters = counters.map((c, i) => { if (i === index) { // Incrementa el contador de clics return c + 1; } else { // El resto no ha cambiado return c; } }); setCounters(nextCounters); } return ( <ul> {counters.map((counter, i) => ( <li key={i}> {counter} <button onClick={() => { handleIncrementClick(i); }}>+1</button> </li> ))} </ul> ); }
Inserting into an array
A veces, es posible que desees insertar un elemento en una posición particular que no esté ni al principio ni al final. Para hacer esto, puedes usar la sintaxis de propagación para arreglos ...
junto con el método slice()
. El método slice()
te permite cortar una “rebanada” de el arreglo. Para insertar un elemento, crearás un arreglo que extienda el segmento antes del punto de inserción, luego el nuevo elemento y luego el resto de la matriz original.
En este ejemplo, el botón “Insertar” siempre inserta en el índice 1
:
import { useState } from 'react'; let nextId = 3; const initialArtists = [ { id: 0, name: 'Marta Colvin Andrade' }, { id: 1, name: 'Lamidi Olonade Fakeye'}, { id: 2, name: 'Louise Nevelson'}, ]; export default function List() { const [name, setName] = useState(''); const [artists, setArtists] = useState( initialArtists ); function handleClick() { const insertAt = 1; // Podría ser cualquier índice const nextArtists = [ // Elementos antes del punto de inserción: ...artists.slice(0, insertAt), // New item: { id: nextId++, name: name }, // Elementos después del punto de inserción: ...artists.slice(insertAt) ]; setArtists(nextArtists); setName(''); } return ( <> <h1>Escultores inspiradores:</h1> <input value={name} onChange={e => setName(e.target.value)} /> <button onClick={handleClick}> Insertar </button> <ul> {artists.map(artist => ( <li key={artist.id}>{artist.name}</li> ))} </ul> </> ); }
Hacer otros cambios en una matriz
Hay algunas cosas que no puedes hacer con la sintaxis extendida y los métodos que no mutan como map()
y filter()
solos. Por ejemplo, es posible que desees invertir u ordenar una matriz. Los métodos JavaScript reverse()
y sort()
están mutando la matriz original, por lo que no puede usarlos directamente.
Sin embargo, puedes copiar el arreglo primero y luego realizar cambios en él.
Por ejemplo:
import { useState } from 'react'; let nextId = 3; const initialList = [ { id: 0, title: 'Big Bellies' }, { id: 1, title: 'Lunar Landscape' }, { id: 2, title: 'Terracotta Army' }, ]; export default function List() { const [list, setList] = useState(initialList); function handleClick() { const nextList = [...list]; nextList.reverse(); setList(nextList); } return ( <> <button onClick={handleClick}> Inverso </button> <ul> {list.map(artwork => ( <li key={artwork.id}>{artwork.title}</li> ))} </ul> </> ); }
Aquí, usa la sintaxis de propagación [...list]
para crear primero una copia de el arreglo original. Ahora que tienes una copia, puedes usar métodos de mutación como nextList.reverse()
o nextList.sort()
, o incluso asignar elementos individuales con nextList[0] = "algo"
.
Sin embargo, incluso si copias un arreglo, no puede mutar los elementos existentes dentro de éste directamente. Esto se debe a que la copia es superficial: el nuevo arreglo contendrá los mismos elementos que la original. Entonces, si modificas un objeto dentro de la matriz copiada, estás mutando el estado existente. Por ejemplo, un código como este es un problema.
const nextList = [...list];
nextList[0].seen = true; // Problema: muta list[0]
setList(nextList);
Aunque nextList
y list
son dos arreglos diferentes, nextList[0]
y list[0]
apuntan al mismo objeto. Entonces, al cambiar nextList[0].seen
, está también cambiando list[0].seen
. ¡Esta es una mutación de estado que debes evitar! Puedes resolver este problema de forma similar a actualizar objetos JavaScript anidados: copiando elementos individuales que deseas cambiar en lugar de mutarlos. Así es cómo.
Actualizando objetos dentro de arreglos
Los objetos no están realmente ubicados “dentro” de los arreglos. Puede parecer que están “dentro” del código, pero cada objeto en un arreglo es un valor separado, al que “apunta” el arreglo. Es por eso que debe tener cuidado al cambiar campos anidados como list[0]
. ¡La lista de obras de arte de otra persona puede apuntar al mismo elemento de el arreglo!
Al actualizar el estado anidado, debe crear copias desde el punto en el que desea actualizar y hasta el nivel superior. Veamos cómo funciona esto.
En este ejemplo, dos listas separadas de ilustraciones tienen el mismo estado inicial. Se supone que deben estar aislados, pero debido a una mutación, su estado se comparte accidentalmente y marcar una casilla en una lista afecta a la otra lista:
import { useState } from 'react'; let nextId = 3; const initialList = [ { id: 0, title: 'Big Bellies', seen: false }, { id: 1, title: 'Lunar Landscape', seen: false }, { id: 2, title: 'Terracotta Army', seen: true }, ]; export default function BucketList() { const [myList, setMyList] = useState(initialList); const [yourList, setYourList] = useState( initialList ); function handleToggleMyList(artworkId, nextSeen) { const myNextList = [...myList]; const artwork = myNextList.find( a => a.id === artworkId ); artwork.seen = nextSeen; setMyList(myNextList); } function handleToggleYourList(artworkId, nextSeen) { const yourNextList = [...yourList]; const artwork = yourNextList.find( a => a.id === artworkId ); artwork.seen = nextSeen; setYourList(yourNextList); } return ( <> <h1>Bucket de arte</h1> <h2>Mi lista de arte para ver:</h2> <ItemList artworks={myList} onToggle={handleToggleMyList} /> <h2>Tu lista de arte para ver:</h2> <ItemList artworks={yourList} onToggle={handleToggleYourList} /> </> ); } function ItemList({ artworks, onToggle }) { return ( <ul> {artworks.map(artwork => ( <li key={artwork.id}> <label> <input type="checkbox" checked={artwork.seen} onChange={e => { onToggle( artwork.id, e.target.checked ); }} /> {artwork.title} </label> </li> ))} </ul> ); }
El problema está en un código como este:
const myNextList = [...myList];
const artwork = myNextList.find(a => a.id === artworkId);
artwork.seen = nextSeen; // Problema: muta un elemento existente
setMyList(myNextList);
Aunque el arreglo myNextList
en sí mismo es nuevo, los elementos iguales son los mismos que en el arreglo myList
original. Entonces, cambiar artwork.seen
cambia el elemento de la obra de arte original. Ese elemento de la obra de arte también está en yourArtworks
, lo que causa el error. Errores como este pueden ser difíciles de pensar, pero afortunadamente desaparecen si evitas el estado de mutación.
Puedes usar map
para sustituir un elemento antiguo con su versión actualizada sin mutación.
setMyList(myList.map(artwork => {
if (artwork.id === artworkId) {
// Crea un *nuevo* objeto con cambios
return { ...artwork, seen: nextSeen };
} else {
// No cambia
return artwork;
}
});
Aquí, ...
es la sintaxis de propagación de objetos utilizada para crear una copia de un objeto.
Con este enfoque, ninguno de los elementos del estado existentes se modifica y el error se soluciona:
import { useState } from 'react'; let nextId = 3; const initialList = [ { id: 0, title: 'Big Bellies', seen: false }, { id: 1, title: 'Lunar Landscape', seen: false }, { id: 2, title: 'Terracotta Army', seen: true }, ]; export default function BucketList() { const [myList, setMyList] = useState(initialList); const [yourList, setYourList] = useState( initialList ); function handleToggleMyList(artworkId, nextSeen) { setMyList(myList.map(artwork => { if (artwork.id === artworkId) { // Crea un *nuevo* objeto con cambios return { ...artwork, seen: nextSeen }; } else { // No cambia return artwork; } })); } function handleToggleYourList(artworkId, nextSeen) { setYourList(yourList.map(artwork => { if (artwork.id === artworkId) { // Crea un *nuevo* objeto con cambios return { ...artwork, seen: nextSeen }; } else { // No cambia return artwork; } })); } return ( <> <h1>Bucket de arte</h1> <h2>Mi lista de arte para ver:</h2> <ItemList artworks={myList} onToggle={handleToggleMyList} /> <h2>Tu lista de arte para ver:</h2> <ItemList artworks={yourList} onToggle={handleToggleYourList} /> </> ); } function ItemList({ artworks, onToggle }) { return ( <ul> {artworks.map(artwork => ( <li key={artwork.id}> <label> <input type="checkbox" checked={artwork.seen} onChange={e => { onToggle( artwork.id, e.target.checked ); }} /> {artwork.title} </label> </li> ))} </ul> ); }
En general, solo debes mutar objetos que acaba de crear. Si estuvieras insertando una nueva obra de arte, podría mutarla, pero si se trata de algo que ya está en estado, debes hacer una copia.
Escribe una lógica de actualización concisa con Immer
Al actualizar arreglos anidados sin mutación puede volverse un poco repetitivo. Al igual que con los objetos:
- En general, no deberías de necesitar actualizar el estado más de un par de niveles de profundidad. Si tus objetos de estado son muy profundos, es posible que desees reestructurarlos de manera diferente para que sean planos.
- Si no deseas cambiar su estructura de estado, puedes preferir usar Immer, que te permite escribir usando la sintaxis conveniente pero cambiante y se encarga de producir las copias para usted.
Aquí está el ejemplo de un Bucket de arte reescrito con Immer:
import { useState } from 'react'; import { useImmer } from 'use-immer'; let nextId = 3; const initialList = [ { id: 0, title: 'Big Bellies', seen: false }, { id: 1, title: 'Lunar Landscape', seen: false }, { id: 2, title: 'Terracotta Army', seen: true }, ]; export default function BucketList() { const [myList, updateMyList] = useImmer( initialList ); const [yourArtworks, updateYourList] = useImmer( initialList ); function handleToggleMyList(id, nextSeen) { updateMyList(draft => { const artwork = draft.find(a => a.id === id ); artwork.seen = nextSeen; }); } function handleToggleYourList(artworkId, nextSeen) { updateYourList(draft => { const artwork = draft.find(a => a.id === artworkId ); artwork.seen = nextSeen; }); } return ( <> <h1>Bucket de arte</h1> <h2>Mi lista de arte para ver:</h2> <ItemList artworks={myList} onToggle={handleToggleMyList} /> <h2>Tu lista de arte para ver:</h2> <ItemList artworks={yourArtworks} onToggle={handleToggleYourList} /> </> ); } function ItemList({ artworks, onToggle }) { return ( <ul> {artworks.map(artwork => ( <li key={artwork.id}> <label> <input type="checkbox" checked={artwork.seen} onChange={e => { onToggle( artwork.id, e.target.checked ); }} /> {artwork.title} </label> </li> ))} </ul> ); }
Ten en cuenta cómo con Immer, la mutación como artwork.seen = nextSeen
ahora está bien:
updateMyTodos(draft => {
const artwork = draft.find(a => a.id === artworkId);
artwork.seen = nextSeen;
});
Esto se debe a que no está mutando el estado original, sino que está mutando un objeto draft
especial proporcionado por Immer. Del mismo modo, puedes aplicar métodos de mutación como push()
y pop()
al contenido del draft
.
Detrás de escena, Immer siempre construye el siguiente estado desde cero de acuerdo con los cambios que ha realizado en el draft
. Esto mantiene sus controladores de eventos muy concisos sin cambiar nunca el estado.
Recapitulación
- Puedes poner arreglos en el estado, pero no puedes cambiarlos.
- En lugar de mutar un arreglo, crea una nueva versión y actualiza el estado.
- Puedes usar la sintaxis de propagación
[...arr, newItem]
para crear arreglos con nuevos elementos. - Puedes usar
filter()
ymap()
para crear nuevos arreglos con elementos filtrados o transformados. - Puedes usar Immer para mantener su código conciso.
Desafío 1 de 4: Actualizar un artículo en el carrito de compras
Completa la lógica handleIncreaseClick
para que al presionar ”+” aumente el número correspondiente:
import { useState } from 'react'; const initialProducts = [{ id: 0, name: 'Baklava', count: 1, }, { id: 1, name: 'Queso', count: 5, }, { id: 2, name: 'Espaguetis', count: 2, }]; export default function ShoppingCart() { const [ products, setProducts ] = useState(initialProducts) function handleIncreaseClick(productId) { } return ( <ul> {products.map(product => ( <li key={product.id}> {product.name} {' '} (<b>{product.count}</b>) <button onClick={() => { handleIncreaseClick(product.id); }}> + </button> </li> ))} </ul> ); }