Mantenere i Componenti Puri

Alcune funzioni JavaScript sono pure. Le funzioni pure eseguono solo un calcolo e nient’altro. Scrivendo rigorosamente i tuoi componenti solo come funzioni pure, puoi evitare un’intera classe di bug confusi e comportamenti imprevedibili man mano che la tua base di codice cresce. Per ottenere questi vantaggi, tuttavia, ci sono alcune regole che devi seguire.

Imparerai

  • Che cosa è la purezza e come ti aiuta ad evitare i bug
  • Come mantenere i componenti puri, mantenendo i cambiamenti fuori dalla fase di renderizzazione
  • Come utilizzare la Strict Mode per trovare errori nei tuoi componenti

Purezza: Componenti Come Formule

In informatica (ed in particolare nel mondo della programmazione funzionale), una funzione pura è una funzione con le seguenti caratteristiche:

  • Si cura solo dei suoi affari. Non modifica oggetti o variabili che esistevano già prima che fosse chiamata.
  • Stessi input, stessi output. Dati gli stessi input, una funzione pura dovrebbe sempre restituire lo stesso risultato.

Potresti avere già familiarità con un esempio di funzioni pure: le formule in matematica.

Considera questa formula matematica: y = 2x.

Se x = 2 allora y = 4. Sempre.

Se x = 3 allora y = 6. Sempre.

Se x = 3, y non sarà a volte 9 o –1 o 2.5 a seconda dell’ora del giorno o dello stato del mercato azionario.

Se y = 2x e x = 3, y sarà sempre 6.

Se trasformassimo questa formula in una funzione JavaScript, apparirebbe così:

function double(number) {
return 2 * number;
}

Nell’esempio sopra, double è una funzione pura. Se gli passi 3, restituirà 6. Sempre.

React è progettato attorno a questo concetto. React presume che ogni componente che scrivi sia una funzione pura. Ciò significa che i componenti React che scrivi devono sempre restituire lo stesso JSX dati gli stessi input:

function Recipe({ drinkers }) {
  return (
    <ol>    
      <li>Fai bollire {drinkers} tazze di acqua.</li>
      <li>Aggiungi {drinkers} cucchiaini di tè e {0.5 * drinkers} cucchiaini di spezie.</li>
      <li>Aggiungi {0.5 * drinkers} tazze di latte da far bollire e zucchero a piacere.</li>
    </ol>
  );
}

export default function App() {
  return (
    <section>
      <h1>Ricetta Chai Speziato</h1>
      <h2>Per due persone</h2>
      <Recipe drinkers={2} />
      <h2>Per un gruppo</h2>
      <Recipe drinkers={4} />
    </section>
  );
}

Quando passi drinkers={2} a Ricetta, questo restituirà JSX contenente 2 tazze di acqua. Sempre.

Se si passa drinkers={4}, questo restituirà JSX contenente 4 tazze di acqua. Sempre.

Proprio come una formula matematica.

Si potrebbe pensare ai propri componenti come ricette: se le si segue e non si introducono nuovi ingredienti durante il processo di cottura, si otterrà lo stesso piatto ogni volta. Quel “piatto” è il JSX che il componente fornisce a React per il render.

A tea recipe for x people: take x cups of water, add x spoons of tea and 0.5x spoons of spices, and 0.5x cups of milk

Illustrato da Rachel Lee Nabors

Side Effects: conseguenze (in)aspettate

Il processo di rendering di React deve sempre essere puro. I componenti dovrebbero solo restituire il loro JSX e non modificare oggetti o variabili che esistevano prima del rendering, in quanto questo li renderebbe impuri!

Ecco un componente che viola questa regola:

let guest = 0;

function Cup() {
  // Non corretto: sta modificando una variabile esistente!
  guest = guest + 1;
  return <h2>Tazza di tè per ospite #{guest}</h2>;
}

export default function TeaSet() {
  return (
    <>
      <Cup />
      <Cup />
      <Cup />
    </>
  );
}

Questo componente legge e scrive una variabile guest dichiarata esternamente. Ciò significa che chiamare questo componente più volte produrrà JSX diversi! E inoltre, se altri componenti leggono guest, produrranno anche loro JSX diversi, a seconda del momento in cui sono stati eseguiti! Ciò non è prevedibile.

Tornando alla nostra formula y = 2x, anche se x = 2, non possiamo essere certi che y = 4. I nostri test potrebbero fallire, gli utenti sarebbero confusi, gli aerei cadrebbero dal cielo - si può vedere come ciò porterebbe a bug confusi!

È possibile risolvere questo componente passando guest come una prop::

function Cup({ guest }) {
  return <h2>Tea cup for guest #{guest}</h2>;
}

export default function TeaSet() {
  return (
    <>
      <Cup guest={1} />
      <Cup guest={2} />
      <Cup guest={3} />
    </>
  );
}

Ora il tuo componente è puro, poiché il JSX che restituisce dipende solo dalla prop guest.

In generale, non devi aspettarti che i tuoi componenti vengano renderizzati in un particolare ordine. Non importa se chiami y = 2x prima o dopo y = 5x: entrambe le formule si risolveranno indipendentemente l’una dall’altra. Allo stesso modo, ogni componente dovrebbe solo “pensare per se stesso”, e non tentare di coordinarsi o dipendere dagli altri durante il rendering. Il rendering è simile a un esame scolastico: ogni componente dovrebbe calcolare JSX da solo!

Approfondimento

Rilevazione dei Calcoli Impuri con StrictMode

Anche se potresti non averne ancora utilizzati tutti, in React ci sono tre tipi di input che puoi leggere durante il rendering: props, state, e context. Dovresti sempre trattare questi input come readonly.

Quando vuoi cambiare qualcosa in risposta all’input dell’utente, dovresti impostare lo state anziché scrivere su una variabile. Non dovresti mai cambiare variabili o oggetti preesistenti durante il rendering del tuo componente.

React offre una “Strict Mode” in cui chiama la funzione di ogni componente due volte durante lo sviluppo. Chiamando la funzione del componente due volte, Strict Mode aiuta a trovare i componenti che infrangono queste regole.

Nota come l’esempio originale abbia visualizzato “Guest #2”, “Guest #4” e “Guest #6” invece di “Guest #1”, “Guest #2” e “Guest #3”. La funzione originale era impura, quindi chiamarla due volte l’ha rotta. Ma la versione pura funziona correttamente anche se la funzione viene chiamata due volte ogni volta. Le funzioni pure calcolano solo, quindi chiamarle due volte non cambierà nulla—proprio come chiamare double(2) due volte non cambia ciò che viene restituito e risolvere y = 2x due volte non cambia il valore di y. Gli stessi input, gli stessi output. Sempre.

Strict Mode non ha alcun effetto in produzione, quindi non rallenterà l’app per i tuoi utenti. Per abilitare Strict Mode, puoi avvolgere il tuo componente principale all’interno di <React.StrictMode>. Alcuni framework lo fanno di default.

Mutazione Locale: il Piccolo Segreto del tuo Componente

Nell’esempio precedente, il problema era che il componente ha cambiato una variabile preesistente durante il rendering. Questo è spesso chiamato una “mutazione” per renderlo un po’ più spaventoso. Le funzioni pure non mutano variabili al di fuori della portata della funzione o oggetti creati prima della chiamata - questo le rende impure!

Tuttavia, è completamente permesso cambiare variabili e oggetti che hai creato appena durante il rendering. In questo esempio, hai creato un’array [], assegnato a una variabile cups, e inserito una dozzina di tazze:

function Cup({ guest }) {
  return <h2>Tazza di tè per ospite #{guest}</h2>;
}

export default function TeaGathering() {
  let cups = [];
  for (let i = 1; i <= 12; i++) {
    cups.push(<Cup key={i} guest={i} />);
  }
  return cups;
}

Se la variabile cups o l’array [] fossero stati creati fuori dalla funzione TeaGathering, questo sarebbe stato un grande problema! Staresti cambiando un oggetto pre-esistente inserendo elementi in quell’array.

Tuttavia, è permesso perché li hai creati durante lo stesso render, all’interno di TeaGathering. Nessun codice al di fuori di TeaGathering saprà mai che questo è accaduto. Questo si chiama “mutazione locale”—è come il piccolo segreto del tuo componente.

Dove puoi causare side effects

Mentre la programmazione funzionale si basa pesantemente sulla purezza, ad un certo punto, in qualche posto, qualcosa deve cambiare. Questo è il punto della programmazione! Questi cambiamenti, come l’aggiornamento dello schermo, l’avvio di un’animazione, la modifica dei dati, sono chiamati side effects. Sono cose che accadono “al margine”, non durante il rendering.

In React, side effect appartengono solitamente agli event handlers. Gli event handler sono funzioni che React esegue quando si esegue un’azione, ad esempio quando si fa clic su un pulsante. Anche se gli event handler sono definiti all’interno del componente, non vengono eseguiti durante il rendering! Quindi gli event handler non devono essere puri

Se hai esaurito tutte le altre opzioni e non riesci a trovare l’event handler giusto per il tuo side effect, puoi comunque allegarlo al tuo JSX restituito con una chiamata useEffect nel tuo componente. Ciò indica a React di eseguirlo in seguito, dopo il rendering, quando sono consentiti side effect. Tuttavia, questo approccio dovrebbe essere l’ultima risorsa.

Quando possibile, cerca di esprimere la tua logica solo con il rendering. Rimarrai sorpreso di quanto lontano questo possa portarti!

Approfondimento

Perché a React importa la purezza?

Scrivere funzioni pure richiede un po’ di abitudine e disciplina. Ma sblocca anche meravigliose opportunità:

  • I tuoi componenti potrebbero funzionare in un ambiente diverso, ad esempio sul server! Poiché restituiscono lo stesso risultato per gli stessi input, un componente può soddisfare molte richieste degli utenti.
  • Puoi migliorare le prestazioni saltando il rendering dei componenti il cui input non è cambiato. Ciò è sicuro perché le funzioni pure restituiscono sempre gli stessi risultati, quindi sono sicure da cache.
  • Se alcuni dati cambiano nel mezzo del rendering di un albero di componenti profondo, React può riavviare il rendering senza perdere tempo per completare il rendering superato. La purezza rende sicura l’interruzione del calcolo in qualsiasi momento.

Ogni nuova funzione React che stiamo costruendo sfrutta la purezza. Dal recupero dei dati alle animazioni alle prestazioni, mantenere i componenti puri sblocca il potere del paradigma React.

Riepilogo

  • Un componente deve essere puro, cioè:
    • Si cura solo dei suoi affari. Non deve cambiare alcun oggetto o variabile esistente prima del rendering.
    • Same inputs, same output. Dati gli stessi input, una funzione pura dovrebbe sempre restituire lo stesso JSX.
  • Il rendering può accadere in qualsiasi momento, quindi i componenti non dovrebbero dipendere dalla sequenza di rendering l’uno dell’altro.
  • Non devi mutare alcuno degli input che i tuoi componenti utilizzano per il rendering. Questo include props, stato e contesto. Per aggiornare lo schermo, imposta (“set”) lo state invece di modificare oggetti pre-esistenti.
  • Cerca di esprimere la logica del tuo componente all’interno del JSX che restituisci. Quando hai bisogno di “cambiare le cose”, di solito vorrai farlo in un gestore di eventi. Come ultima risorsa, puoi utilizzare useEffect.
  • Scrivere funzioni pure richiede un po’ di pratica, ma sblocca il potere del paradigma di React.

Sfida 1 di 3:
Riparare un orologio rotto

Questo componente tenta di impostare la classe CSS dell’<h1> a "night" durante l’intervallo di tempo dalla mezzanotte alle sei del mattino, ed a "day" in tutti gli altri momenti. Tuttavia, non funziona. Puoi correggerlo?

Puoi verificare se la tua soluzione funziona cambiando temporaneamente il fuso orario del computer. Quando l’ora corrente è compresa tra la mezzanotte e le sei del mattino, l’orologio dovrebbe avere i colori invertiti.

export default function Clock({ time }) {
  let hours = time.getHours();
  if (hours >= 0 && hours <= 6) {
    document.getElementById('time').className = 'night';
  } else {
    document.getElementById('time').className = 'day';
  }
  return (
    <h1 id="time">
      {time.toLocaleTimeString()}
    </h1>
  );
}