Scrivere JavaScript più affidabile con lo stato immutabile

by Anastasia P.
SHARE
Scrivere JavaScript più affidabile con lo stato immutabile
© Guida-HTML5.it

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:

  • map per trasformare gli elementi;
  • filter per rimuovere elementi;
  • concat o lo spread per aggiungere elementi;
  • slice per 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 const a let: const impedisce di riassegnare il riferimento e comunica meglio l’intenzione del codice. Ricorda però che gli oggetti dichiarati con const possono comunque essere modificati internamente.
  • Non usare Object.freeze come 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, reverse e splice modificano 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 structuredClone per 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.

Approfondisci con risorse ufficiali

SHARE