CONTATTACI

Guide per aspiranti programmatori

Programmatore che esamina react al microscopio
Lezione 32 / 41

Quando evitare gli effetti collaterali in React

Gli effetti collaterali sono il concetto più mal interpretato e impropriamente usato di React. Vediamo una lista di casi in cui potresti essere tentato ad usare un effetto collaterale, e come evitare di farlo.

In generale, ci sono due casi in cui gli effetti collaterali non servono mai:

  • per trasformare i dati in JSX
  • per gestire eventi

Vediamoli!

 

Trasformare i dati in JSX

Immaginiamo di voler filtrare una lista prima di mostrarla. La lista arriva tramite prop da un componente controller e noi vogliamo mostrare solo alcuni degli elementi. Potremmo essere tentati di creare uno stato, che rappresenta la “lista filtrata”, e utilizzare un effetto collaterale per modificarla ogni volta che cambia la lista originale (che sarebbe la prop):

 

export function MyList({ items }) {
  const [filteredItems, setFilteredItems] = useState(items);
  const [chosenCategory, setChosenCategory] = useState("all");

  useEffect(() => {
    if (chosenCategory === "all") {
      setFilteredItems(items);
    } else {
      const filteredItems = items.filter((item) => {
        return item.category === chosenCategory;
      });

      setFilteredItems(filteredItems);
    }
  }, [items, chosenCategory]);

  // ...
}

In questo esempio, abbiamo due stati interni, che rappresentano una categoria scelta dall’utente e la lista filtrata per categoria. Quando la categoria scelta dall’utente (che possiamo immaginare essere scelta tramite un menù a tendina) cambia, scateniamo un effetto collaterale che modifica la lista filtrata. Allo stesso modo, quando la lista di partenza (items, che arriva tramite props) cambia, scateniamo lo stesso effetto collaterale.

Vediamo come React gestirebbe i cicli di rendering in questo caso:

  1. Ciclo di rendering iniziale.
  2. Ciclo di rendering quando viene chiamata la funzione setter setChosenCategory, oppure quando si aggiorna la prop items.
  3. Ciclo di rendering subito dopo il secondo, perché i cambiamenti di chosenCategory e items scatenano useEffect, che a sua volta chiama setFilteredItems.

Il terzo ciclo di rendering non è necessario! filteredItems dipende direttamente da items e il nostro componente MyList viene già chiamato al rendering ogni volta che items si aggiorna:

 

export function MyList({ items }) {
  const [chosenCategory, setChosenCategory] = useState("all");

  const filteredItems = (() => {
    if (chosenCategory === "all") {
      return items;
    } else {
      return filteredItems.filter((item) => {
        return item.category === chosenCategory;
      });
    }
  })();

  // ...
}

Gestire eventi

Il secondo caso generico è la gestione di operazioni che dipendono direttamente da eventi scatenati dall’utente. Per esempio, immaginiamo di avere un hook useNotification che ci permette di mostrare una notifica, e un bottone “Compra”: quando il bottone “Compra” viene attivato, mandiamo una richiesta di rete di tipo command. Se tutto va bene, inviamo una notifica di successo per far sapere all’utente che tutto è andato a buon fine. Utilizziamo il nostro hook useCommand per l’esempio:

 

export default function BuyButton({ product }) {
  const [buyRequestNetworkState, sendBuyRequest] = useCommand({
    path: "/products/buy",
    method: "POST",
  });

  const { showNotification } = useNotification();

  useEffect(() => {
    if (buyRequestNetworkState.isSuccessful()) {
      showNotification({
        type: "success",
        message: "You just bought the product!",
      });
    }
  }, [buyRequestNetworkState]);

  return (
    <button
      disabled={buyRequestNetworkState.isLoading()}
      onClick={() => sendBuyRequest(product)}
    >
      Buy
    </button>
  );
}

In questo esempio, al click del bottone che dice “Buy”, inviamo una richiesta di tipo POST a una API al percorso /products/buy. Utilizziamo un effetto collaterale per mostrare la notifica di successo ogni volta che la richiesta di rete buyRequestNetworkState viene aggiornata. Se la richiesta è andata a buon fine, mostriamo la notifica.

Anche in questo caso, stiamo scatenando un ciclo di rendering in più rispetto a quello che ci serve. Possiamo rimuovere l’effetto collaterale e chiamare showNotification direttamente dentro una funzione per gestire l’evento, sfruttando il fatto che sendBuyRequest restituisce una Promise:

 

export default function BuyButton({ product }) {
  const [buyRequestNetworkState, sendBuyRequest] = useCommand({
    path: "/products/buy",
    method: "POST",
  });

  const { showNotification } = useNotification();

  const onButtonClick = () => {
    sendBuyRequest(product).then(() => {
      showNotification({
        type: "success",
        message: "You just bought the product!",
      });
    });
  };

  return (
    <button
      disabled={buyRequestNetworkState.isLoading()}
      onClick={onButtonClick}
    >
      Buy
    </button>
  );
}

Questi erano due casi generici in cui si può evitare di utilizzare effetti collaterali, ma non temere, abbiamo anche un sacco di esempi specifici! Vediamone un paio!

 

Aggiornare stati in base a props o altri stati

Un esempio classico: abbiamo nome e cognome, vogliamo metterli insieme per ottenere il nome completo. Potresti essere tentato di fare una cosa del genere:

 

export default function Form() {
  const [firstName, setFirstName] = useState("");
  const [lastName, setLastName] = useState("");

  const [fullName, setFullName] = useState("");

  useEffect(() => {
    setFullName(firstName + " " + lastName);
  }, [firstName, lastName]);

  // ...
}

Aggiungendo fullName come stato, dobbiamo, poi, preoccuparci di tenerlo sincronizzato con i due stati da cui deriva (firstName e lastName), ma non ci serve farlo.

Anche in questo caso, possiamo risparmiare uno stato e un ciclo di rendering utilizzando una banale costante. In più, firstName e lastName descrivono due proprietà della stessa cosa (tendenzialmente una persona) e abbiamo detto che gli stati dovrebbero rappresentare tutte e sole le informazioni che servono per descrivere la realtà:

 

export default function Form() {
  const [person, setPerson] = useState({
    firstName: "",
    lastName: "",
  });

  const fullName = person.firstName + " " + person.lastName;

  // ...
}

fullName dipende direttamente dallo stato di person, ci penserà React a chiamare la nostra funzione/componente Form quando verrà chiamata la funzione setPerson, così che fullName venga aggiornata.

 

Notificare il componente controller

Quando si parla di input (nel senso di elementi <input />), non sempre è possibile operare con un solo stato. Idealmente, lo stato e la sua funzione setter esistono una sola volta in un solo componente e vengono passati insieme ai componenti view.

Ci sono, però, dei casi in cui lo stato e l’input non coincidono, per esempio perché lo stato deve passare una validazione ma vogliamo che l’utente veda quello che sta scrivendo anche se è sbagliato, oppure perché l’utente sta scrivendo in un campo di ricerca e non vogliamo lanciare una chiamata di rete ogni volta che l’input cambia, ma solo quando l’utente ha finito di scrivere.

In questi casi, abbiamo bisogno di una prop e un evento per notificare il componente controller, ma anche di uno stato interno per tenere traccia dei cambiamenti. In questo caso, la tentazione di tenere sincronizzati lo stato con le prop è forte:

 

export default function MyInput({ value, validate, onChange }) {
  const [state, setState] = useState(value);

  useEffect(() => {
    const isValid = validate(state);

    if (isValid) {
      onChange(state);
    }
  }, [state, validate, onChange]);

  return (
    <input
      value={state}
      onInput={(event) => {
        setState(event.currentTarget.value);
      }}
    />
  );
}

Anche in questo caso, possiamo spostare entrambi gli aggiornamenti in una funzione per gestire l’evento:

 

export default function MyInput({ value, validate, onChange }) {
  const [state, setState] = useState(value);

  const onInput = (inputValue) => {
    setState(inputValue);

    const isValid = validate(state);

    if (isValid) {
      onChange(state);
    }
  };

  return (
    <input
      value={state}
      onInput={(event) => {
        onInput(event.currentTarget.value);
      }}
    />
  );
}

Se, invece, possiamo “sollevare lo stato” e far gestire tutto al componente controller, quella è un’opzione migliore, perché ci permette di isolare la logica di funzionamento in un unico punto. 

Contattaci senza impegno per informazioni sul corso

Pagamento rateale

Valore della rata: A PARTIRE DA 115 €/mese.

Esempio di finanziamento 

Importo finanziato: € 2440 in 24 rate da € 115 – TAN fisso 9,55% TAEG 12,57% – importo totale del credito € 2841.

Il costo totale del credito comprende: interessi calcolati al TAN indicato, oneri fiscali (imposta di bollo sul contratto 16,00 euro*) addebitati sulla prima rata, costo mensile di gestione pratica € 3,90, spesa di istruttoria € 0,00, spesa per invio rendicontazione periodica cartacea € 0,98 (o spesa per invio rendicontazione periodica cartacea € 0,00), imposta di bollo su rendicontazione periodica € 0,00. Modalità di rimborso obbligatoria: addebito diretto su c/c. La scadenza delle rate è determinata dal giorno della liquidazione del contratto; la data di scadenza delle rate è prevista il giorno 15 del mese. L’importo di ciascuna rata comprende una quota di capitale crescente e interessi decrescente secondo un piano di ammortamento “alla francese”. Offerta valida dal 01/01/2024 al 31/12/2024.

Messaggio pubblicitario con finalità promozionale. Per le informazioni precontrattuali richiedere sul punto vendita il documento “Informazioni europee di base sul credito ai consumatori” (SECCI) e copia del testo contrattuale. Salvo approvazione di Sella Personal Credit S.p.A. Aulab S.r.l. opera quale intermediario del credito NON in esclusiva.

*In fase di richiesta del finanziamento verrà proposta la facoltà di selezionare, in alternativa all’imposta di bollo sul contratto di 16,00 euro, l’imposta sostitutiva, pari allo 0,25% dell’importo finanziato.

Pagamento rateale

Valore della rata: A PARTIRE DA 210 €/mese.

Esempio di finanziamento  

Importo finanziato: € 4500 in 24 rate da € 210,03 – TAN fisso 9,68% TAEG 11,97% – importo totale del credito € 5146,55.

Il costo totale del credito comprende: interessi calcolati al TAN indicato, oneri fiscali (imposta di bollo sul contratto 16,00 euro*) addebitati sulla prima rata, costo mensile di gestione pratica € 3,90, spesa di istruttoria € 0,00, spesa per invio rendicontazione periodica cartacea € 0,98 (o spesa per invio rendicontazione periodica cartacea € 0,00), imposta di bollo su rendicontazione periodica € 0,00. Modalità di rimborso obbligatoria: addebito diretto su c/c. La scadenza delle rate è determinata dal giorno della liquidazione del contratto; la data di scadenza delle rate è prevista il giorno 15 del mese. L’importo di ciascuna rata comprende una quota di capitale crescente e interessi decrescente secondo un piano di ammortamento “alla francese”. Offerta valida dal 01/01/2024 al 31/12/2024.

Messaggio pubblicitario con finalità promozionale. Per le informazioni precontrattuali richiedere sul punto vendita il documento “Informazioni europee di base sul credito ai consumatori” (SECCI) e copia del testo contrattuale. Salvo approvazione di Sella Personal Credit S.p.A. Aulab S.r.l. opera quale intermediario del credito NON in esclusiva.

* In fase di richiesta del finanziamento verrà proposta la facoltà di selezionare, in alternativa all’imposta di bollo sul contratto di 16,00 euro, l’imposta sostitutiva, pari allo 0,25% dell’importo finanziato.

Contattaci senza impegno per informazioni sul corso

Scopriamo insieme se i nostri corsi fanno per te. Compila il form e aspetta la chiamata di uno dei nostri consulenti.