In React aggiorni un array o un oggetto nello stato creandone uno nuovo e passandolo al setter. Non modificare mai il valore esistente sul posto: usa lo spread (...), map, filter o slice per costruire una copia con la tua modifica, poi chiama setItems(copy).
[...items, { ... }] è un nuovo array con un elemento in più, e filter restituisce un nuovo array senza quello rimosso. Entrambi lasciano stare il vecchio array.
Perché la mutazione non provoca un rendering
Quando chiami un setter, React confronta il nuovo valore con quello vecchio usando Object.is. Per array e oggetti questo confronta i riferimenti, non i contenuti. Se modifichi un array con push e ripassi lo stesso array, React vede lo stesso riferimento, decide che non è cambiato nulla e salta il rendering.
Clicca "push" qualche volta. La console mostra l'array che cresce, ma non compare alcuna nuova riga di rendering e il testo resta a. Poi clicca "spread": avviene un rendering e tutti gli elementi aggiunti con push compaiono in una volta, perché erano nell'array da sempre. La mutazione non genera errori, lascia solo lo schermo non aggiornato finché qualcos'altro non provoca un rendering. Clicca "push" ancora una volta dopo questo e stavolta potrebbe esserci un rendering: React non riesce sempre a saltare il lavoro in anticipo, quindi una mutazione a volte si vede e altre no. Questa imprevedibilità è il motivo per non modificare lo stato direttamente.
Array: metodi da usare e da evitare
La regola è semplice: usa i metodi che restituiscono un nuovo array, evita quelli che modificano l'array su cui vengono chiamati.
| Obiettivo | Evita (modifica) | Usa (restituisce un nuovo array) |
|---|---|---|
| Aggiungere | push, unshift | [...arr, item], [item, ...arr] |
| Rimuovere | pop, shift, splice | filter, slice |
| Sostituire | arr[i] = x, splice | map, with(i, x) |
| Inserire | splice | [...arr.slice(0, i), x, ...arr.slice(i)], toSpliced |
| Ordinare, invertire | sort, reverse | toSorted, toReversed, oppure prima copia |
toSorted, toReversed, toSpliced e with sono metodi degli array più recenti (ES2023), supportati da tutti i principali browser attuali dal 2023. Fanno lo stesso lavoro dei loro cugini che modificano l'array, ma su una copia.
Aggiornare una voce con map
Per cambiare una voce, applica map all'array e restituisci un nuovo oggetto per la voce che vuoi, e l'oggetto originale per tutte le altre.
La parte { ...todo, done: !todo.done } è importante. Un bug comune è copiare l'array ma modificare l'oggetto al suo interno:
const next = [...todos];
next[0].done = true; // next is new, but next[0] is the same object as todos[0]
setTodos(next);
Questo provoca un rendering, quindi sembra a posto, ma il vecchio array e quello nuovo ora condividono un oggetto modificato. Qualsiasi cosa confronti la vecchia voce con la nuova (un figlio con React.memo, un effetto che dipende dalla voce) non vede alcuna differenza. Copia la voce che cambi.
Inserire in una posizione
Non esiste un insert che non modifichi l'array, quindi taglia l'array in due e metti il nuovo elemento tra le due metà. toSpliced(index, 0, item) fa lo stesso in una sola chiamata.
Imposta l'indice a 0 e inserisci di nuovo: la nuova C va in testa. Sostituisci l'espressione next con letters.toSpliced(index, 0, 'C') e il risultato è lo stesso.
Ordinare e invertire
sort e reverse modificano l'array su cui vengono chiamati, anche se lo restituiscono pure. Questo rende setItems(items.sort()) una mutazione seguita da un set con lo stesso riferimento, che è il bug dell'inizio di questa pagina. Ordina invece una copia.
[...scores].reverse() funziona perché lo spread crea prima la copia e poi reverse modifica solo quella copia.
Aggiornare oggetti e oggetti annidati
Gli oggetti seguono la stessa regola. Fai lo spread del vecchio oggetto in uno nuovo e sovrascrivi i campi che cambiano. Per un oggetto annidato, fai lo spread a ogni livello scendendo fino al campo: un solo spread copia solo il livello più alto, quindi l'oggetto interno resterebbe quello vecchio.
Ogni livello che copi è un nuovo oggetto; ogni livello che non tocchi resta lo stesso oggetto. È questo che rende lo spread economico: se lo stato contenesse anche una lunga lista di ordini, { ...user } copierebbe il riferimento alla lista, non la lista stessa.
Se un oggetto nello stato è annidato su tre o quattro livelli e continui a scrivere lunghe catene di spread, spesso è segno che lo stato potrebbe essere più piatto. Memorizzare le voci in una tabella per id, o dividere un grande oggetto in alcune chiamate a useState, mantiene ogni aggiornamento breve. Quando molti eventi cambiano una stessa struttura, aiuta anche un reducer (vedi useReducer).
structuredClone vs spread
structuredClone(user) crea una copia profonda in una sola chiamata, così puoi poi modificare liberamente la copia:
const next = structuredClone(user);
next.address.city = 'Oslo';
setUser(next);
È corretto, ma copia l'intero oggetto ogni volta, comprese le parti che non hai cambiato. Questo costa tempo su uno stato grande, e significa che ogni oggetto annidato riceve un nuovo riferimento, quindi un figlio memorizzato che riceve user.orders esegue di nuovo il rendering anche se gli ordini non sono cambiati. structuredClone inoltre genera un errore sulle funzioni, e le istanze di classe tornano come semplici oggetti. Lo spread copia solo il percorso che cambi, quindi è la scelta abituale; tieni structuredClone per il raro aggiornamento che riscrive davvero la maggior parte dell'oggetto.
Scrivere mutazioni con Immer
Immer è una piccola libreria che ti permette di scrivere codice che sembra una mutazione. Ti consegna una bozza, registra cosa cambi e produce un nuovo valore immutabile copiando solo le parti modificate. Il pacchetto use-immer la racchiude in un hook:
import { useImmer } from 'use-immer';
function Profile() {
const [user, updateUser] = useImmer({
name: 'Ada',
address: { city: 'London' },
});
function moveTo(city) {
updateUser((draft) => {
draft.address.city = city;
});
}
// ...
}
Immer non è disponibile negli editor di questa pagina, che caricano solo React. Vale la pena aggiungerla quando il tuo stato è annidato in profondità e gli spread diventano difficili da leggere. Per uno stato piatto bastano spread e map, ed è ciò che vedrai nella maggior parte del codice React.
Domande frequenti
Come si aggiunge un elemento a un array nello stato React?
Crea un nuovo array che contiene i vecchi elementi più quello nuovo e passalo al setter: setItems([...items, newItem]). Non chiamare items.push(newItem), perché modifica l'array esistente e React vede lo stesso riferimento.
Perché il mio componente non esegue un nuovo render quando aggiorno un array?
Molto probabilmente hai modificato l'array sul posto (push, splice, sort o arr[i] = x) e hai ripassato lo stesso array. React confronta il vecchio e il nuovo valore con Object.is, trova lo stesso oggetto e salta il rendering. Passa invece un nuovo array.
Come si aggiorna un oggetto dentro un array in React?
Usa map e restituisci un nuovo oggetto per l'elemento che cambia, e l'elemento originale per tutti gli altri: setTodos(todos.map(t => t.id === id ? { ...t, done: !t.done } : t)).
Come si aggiorna un oggetto annidato nello stato React?
Copia ogni livello lungo il percorso fino al campo che cambi: setUser({ ...user, address: { ...user.address, city: 'Oslo' } }). Un solo spread copia solo il livello più alto, quindi l'oggetto interno resterebbe condiviso.
Per lo stato React conviene structuredClone o lo spread operator?
Preferisci lo spread. Copia solo ciò che cambia e mantiene le parti non toccate come gli stessi oggetti, il che aiuta React.memo e i confronti. structuredClone copia tutto ogni volta, il che è più semplice da scrivere ma più lento su uno stato grande e rompe l'uguaglianza dei riferimenti per le parti che non sono cambiate.