Introduzione
Quando si sviluppa un’interfaccia JavaScript, è frequente dover modificare lo stato dell’applicazione: un carrello, una lista di preferiti, le impostazioni dell’utente o i dati visualizzati in una dashboard. Una buona pratica consiste nel trattare lo stato come immutabile: invece di modificare direttamente un oggetto o un array esistente, si crea una nuova versione contenente i cambiamenti.
Destructuring assignment, spread operator e rest operator rendono questo approccio molto leggibile. Il caso pratico di questo tutorial è un piccolo carrello della spesa gestito attraverso una funzione reducer. La funzione riceve lo stato precedente e un’azione, quindi restituisce un nuovo stato senza alterare quello originale.
Questa tecnica è utile anche senza utilizzare framework specifici. In React, Redux, Vue o applicazioni JavaScript tradizionali, l’aggiornamento immutabile facilita il confronto tra vecchio e nuovo stato e riduce gli effetti collaterali difficili da individuare.
Codice completo
// Stato iniziale del carrello
const initialState = {
items: [
{
id: 1,
name: "Tastiera meccanica",
price: 79.90,
quantity: 1
},
{
id: 2,
name: "Mouse wireless",
price: 29.90,
quantity: 2
}
],
coupon: null
};
// Calcola il totale senza modificare lo stato ricevuto
function calculateTotal(items) {
return items.reduce(
(total, { price, quantity }) => total + price * quantity,
0
);
}
// Aggiorna il carrello restituendo sempre un nuovo oggetto
function cartReducer(state, action) {
// Destructuring dello stato: estraiamo solo ciò che serve
const { items, coupon } = state;
switch (action.type) {
case "ADD_ITEM": {
const { product } = action;
// Controlliamo se il prodotto è già presente
const existingItem = items.find(({ id }) => id === product.id);
let updatedItems;
if (existingItem) {
// Creiamo un nuovo array e sostituiamo solo l´articolo interessato
updatedItems = items.map(item =>
item.id === product.id
? { ...item, quantity: item.quantity + 1 }
: item
);
} else {
// Aggiungiamo una copia del prodotto con quantità iniziale
updatedItems = [
...items,
{ ...product, quantity: 1 }
];
}
return {
...state,
items: updatedItems
};
}
case "REMOVE_ITEM": {
const { productId } = action;
// Restituisce tutti gli articoli tranne quello da rimuovere
const updatedItems = items.filter(({ id }) => id !== productId);
return {
...state,
items: updatedItems
};
}
case "CHANGE_QUANTITY": {
const { productId, quantity } = action;
// Evitiamo quantità non valide
if (quantity < 1) {
return state;
}
const updatedItems = items.map(item =>
item.id === productId
? { ...item, quantity }
: item
);
return {
...state,
items: updatedItems
};
}
case "APPLY_COUPON": {
const { code, discount } = action;
return {
...state,
coupon: {
code,
discount
}
};
}
case "CLEAR_COUPON": {
// Rest operator: separiamo coupon dal resto dello stato
const { coupon: removedCoupon, ...stateWithoutCoupon } = state;
return {
...stateWithoutCoupon,
coupon: null
};
}
default:
return state;
}
}
// Esempio di utilizzo
let state = initialState;
state = cartReducer(state, {
type: "ADD_ITEM",
product: {
id: 3,
name: "Webcam HD",
price: 49.90
}
});
state = cartReducer(state, {
type: "CHANGE_QUANTITY",
productId: 1,
quantity: 2
});
state = cartReducer(state, {
type: "APPLY_COUPON",
code: "DEV10",
discount: 10
});
console.log("Carrello aggiornato:", state);
console.log("Totale:", calculateTotal(state.items)); Spiegazione
Destructuring degli oggetti
Nel reducer, l’istruzione const { items, coupon } = state; estrae le proprietà necessarie senza dover ripetere continuamente state.items e state.coupon. Lo stesso schema viene usato nei callback di find, map e reduce:
items.find(({ id }) => id === product.id); Il parametro della funzione è direttamente destrutturato. Invece di ricevere un oggetto chiamato item e poi leggere item.id, si ottiene immediatamente la proprietà id.
Spread per creare nuovi oggetti
Nel caso seguente:
{ ...item, quantity: item.quantity + 1 } lo spread copia le proprietà dell’oggetto originale. La proprietà quantity viene poi riscritta con il nuovo valore. L’ordine è importante: le proprietà successive sovrascrivono quelle precedenti.
Lo stesso principio viene applicato allo stato completo:
return {
...state,
items: updatedItems
}; Tutte le proprietà esistenti vengono mantenute, mentre items viene sostituito con il nuovo array.
Spread per gli array
Per aggiungere un elemento senza usare push, si crea un nuovo array:
const updatedItems = [
...items,
{ ...product, quantity: 1 }
]; push modificherebbe direttamente l’array originale. Lo spread, invece, copia gli elementi esistenti e aggiunge il nuovo elemento alla fine.
Rest per escludere proprietà
Il rest operator ha la stessa sintassi dello spread, ma in un’assegnazione di destructuring raccoglie le proprietà rimanenti:
const { coupon: removedCoupon, ...stateWithoutCoupon } = state; In questo esempio, coupon viene estratto e rinominato in removedCoupon, mentre tutte le altre proprietà finiscono in stateWithoutCoupon. Anche se la variabile estratta non viene utilizzata successivamente, l’operazione permette di costruire chiaramente una copia dello stato senza quella proprietà.
Perché usare map e filter?
map è ideale quando bisogna trasformare uno o più elementi mantenendo la struttura dell’array. filter è indicato quando bisogna rimuovere elementi. Entrambi restituiscono nuovi array e non modificano quello originale, caratteristica fondamentale per uno stato immutabile.
Best practice
- Non usare push, pop o splice direttamente sugli array dello stato. Preferisci spread,
mapefilter. - Non modificare proprietà annidate direttamente. Crea una nuova copia a ogni livello necessario.
- Destruttura solo ciò che ti serve. Questo migliora la leggibilità e rende più chiaro il contratto della funzione.
- Mantieni le azioni esplicite. Un’azione come
CHANGE_QUANTITYcomunica meglio l’intenzione rispetto a una modifica generica. - Valida i dati prima di creare il nuovo stato. Nel codice, una quantità inferiore a uno viene rifiutata.
- Ricorda che lo spread è una copia superficiale. Gli oggetti annidati condividono ancora i riferimenti, quindi devono essere copiati esplicitamente quando vengono modificati.
- Restituisci lo stato originale quando non c’è alcuna modifica. Questo evita lavoro inutile e mantiene un comportamento prevedibile.
Riepilogo
Destructuring, spread e rest operator non sono soltanto scorciatoie sintattiche. Insieme permettono di progettare aggiornamenti dello stato più sicuri, leggibili e prevedibili.
Il destructuring estrae rapidamente proprietà da oggetti e valori da array. Lo spread crea nuovi oggetti o array partendo da dati esistenti. Il rest raccoglie le proprietà rimanenti, risultando utile per escludere o separare parti dello stato.
Combinando questi strumenti con map, filter e reduce, è possibile realizzare reducer efficaci senza modificare direttamente i dati originali. Questa impostazione costituisce una base importante per applicazioni JavaScript robuste e facilmente testabili.
Approfondisci con risorse ufficiali
- MDN – Destructuring assignment: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring
- MDN – Spread syntax: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax
- MDN – Rest parameters: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/rest_parameters
- MDN – Array.prototype.map: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map
- MDN – Array.prototype.filter: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter
