Introduzione
Uno degli aspetti più importanti del codice pulito in JavaScript è il modo in cui vengono gestiti i dati durante l’esecuzione del programma. Un errore molto comune consiste nel modificare direttamente oggetti e array già esistenti. Questa tecnica può sembrare comoda, ma rende il comportamento del programma più difficile da prevedere, soprattutto quando lo stesso dato viene utilizzato da più funzioni o componenti.
La gestione immutabile dello stato consiste nel trattare i dati esistenti come non modificabili. Quando serve un cambiamento, si crea una nuova versione dell’oggetto o dell’array, lasciando intatto il valore originale. Questo approccio migliora la leggibilità, facilita il debugging e riduce gli effetti collaterali nascosti.
L’immutabilità non significa che JavaScript renda automaticamente ogni valore immutabile. È una convenzione di progettazione che possiamo applicare usando strumenti come const, l’operatore spread, map, filter e structuredClone.
Codice completo
const initialState = {
tasks: [
{ id: 1, title: "Studiare JavaScript", completed: false },
{ id: 2, title: "Scrivere un esercizio", completed: false }
],
filter: "all"
};
// Crea una nuova lista senza modificare quella originale
function addTask(state, title) {
const newTask = {
id: Date.now(),
title,
completed: false
};
return {
...state,
tasks: [...state.tasks, newTask]
};
}
// Aggiorna solo l´attività con l´id indicato
function toggleTask(state, taskId) {
return {
...state,
tasks: state.tasks.map(task => {
if (task.id !== taskId) {
return task;
}
return {
...task,
completed: !task.completed
};
})
};
}
// Rimuove un´attività creando un nuovo array
function removeTask(state, taskId) {
return {
...state,
tasks: state.tasks.filter(task => task.id !== taskId)
};
}
// Modifica il filtro senza alterare lo stato precedente
function setFilter(state, filter) {
return {
...state,
filter
};
}
let state = initialState;
state = addTask(state, "Ripassare gli array");
state = toggleTask(state, 1);
state = setFilter(state, "completed");
console.log("Stato iniziale:", initialState);
console.log("Stato corrente:", state); Spiegazione
Perché evitare la modifica diretta
Consideriamo questo esempio:
const user = {
name: "Giulia",
preferences: {
theme: "light"
}
};
function enableDarkMode(user) {
user.preferences.theme = "dark";
return user;
}
const updatedUser = enableDarkMode(user);
console.log(user.preferences.theme); // "dark" La funzione ha modificato direttamente l’oggetto ricevuto. Di conseguenza, anche la variabile user è cambiata. Se un’altra parte dell’applicazione utilizzava lo stesso riferimento aspettandosi il tema "light", potrebbe verificarsi un comportamento inatteso.
Con un approccio immutabile, invece, si crea una nuova struttura:
function enableDarkMode(user) {
return {
...user,
preferences: {
...user.preferences,
theme: "dark"
}
};
}
const user = {
name: "Giulia",
preferences: {
theme: "light"
}
};
const updatedUser = enableDarkMode(user);
console.log(user.preferences.theme); // "light"
console.log(updatedUser.preferences.theme); // "dark" L’operatore spread copia le proprietà del primo livello. Per questo, quando si aggiorna un oggetto annidato, è necessario copiare anche il livello interno che deve cambiare. Questa tecnica viene chiamata spesso structural sharing: vengono riutilizzate le parti che non cambiano e create nuove copie solo dove necessario.
Array immutabili
Alcuni metodi degli array modificano direttamente l’array originale. Tra questi troviamo push, pop, splice e sort. Per aggiornare uno stato senza mutarlo, è preferibile usare metodi come:
mapper trasformare gli elementi;filterper rimuovere elementi;concato lo spread per aggiungere elementi;sliceper creare una porzione senza modificare l’originale.
const numbers = [3, 1, 2];
// Non modifica numbers
const sortedNumbers = [...numbers].sort((a, b) => a - b);
console.log(numbers); // [3, 1, 2]
console.log(sortedNumbers); // [1, 2, 3] Best practice
- Preferisci
constalet:constimpedisce di riassegnare il riferimento e comunica meglio l’intenzione del codice. Ricorda però che gli oggetti dichiarati conconstpossono comunque essere modificati internamente. - Non usare
Object.freezecome unica protezione: il congelamento è superficiale e non protegge automaticamente gli oggetti annidati. Può essere utile per configurazioni semplici, ma non sostituisce una strategia coerente. - Evita copie inutili: non è necessario clonare l’intero stato a ogni operazione. Copia soltanto i livelli che cambiano e riutilizza quelli invariati.
- Attenzione ai metodi mutabili:
sort,reverseesplicemodificano l’array originale. Crea prima una copia quando lavori su dati condivisi. - Usa funzioni di aggiornamento prevedibili: una funzione che riceve uno stato e restituisce un nuovo stato è più semplice da testare rispetto a una funzione che modifica variabili esterne.
- Considera
structuredCloneper copie profonde: è utile per strutture complesse, ma può essere più costoso della copia selettiva con spread e non supporta ogni tipo speciale allo stesso modo. - Non confondere immutabilità e prestazioni: creare copie ha un costo, ma una struttura ben progettata riduce spesso il lavoro complessivo del programma e rende più efficienti i controlli basati sui riferimenti.
Questo approccio è particolarmente utile nelle interfacce grafiche, nei reducer, nei sistemi con cronologia delle modifiche e in tutte le applicazioni dove più parti del programma osservano lo stesso stato.
Riepilogo
Scrivere codice JavaScript pulito non significa soltanto usare una sintassi corretta. Significa anche rendere chiaro come cambiano i dati nel tempo. La gestione immutabile dello stato evita modifiche nascoste, rende le funzioni più prevedibili e semplifica i test.
Per applicarla, crea nuovi oggetti e array invece di modificare quelli esistenti, usa metodi non mutabili come map e filter, copia correttamente gli oggetti annidati e presta attenzione ai metodi che alterano direttamente il contenuto. In questo modo il codice diventa più facile da comprendere, mantenere e correggere.
