Lo stato come un'istantanea

Le variabili di stato possono sembrare delle normali variabili JavaScript su cui è possibile leggere e scrivere. Tuttavia lo stato si comporta più come un’istantanea. Quando lo si assegna, non si modifica la variabile di stato che si ha già, ma si innesca un nuovo renderizzato.

Imparerai

  • Come l’assegnazione dello stato innesca re-renderizzazioni
  • Come e quando viene aggiornato lo stato
  • Perché lo stato non viene aggiornato immediatamente dopo averlo assegnato
  • Come i gestori di eventi accedono ad un‘“istantanea” dello stato

Come l’assegnazione dello stato innesca re-renderizzazioni

Si potrebbe pensare che l’interfaccia dell’utente cambi direttamente in risposta ad un evento dell’utente stesso, come un click. In React funziona in modo leggermente diverso da questo modello mentale. Nella pagina precedente si è visto che quando si assegna lo stato si richiede un nuovo renderizzato da React. Questo significa che per far reagire l’interfaccia al evento, è neccessario aggiornare lo stato. In questo esempio, quando premi “Send”, setIsSent(true) dice a React di re-renderizzare la UI.

import { useState } from 'react';

export default function Form() {
  const [isSent, setIsSent] = useState(false);
  const [message, setMessage] = useState('Hi!');
  if (isSent) {
    return <h1>Your message is on its way!</h1>
  }
  return (
    <form onSubmit={(e) => {
      e.preventDefault();
      setIsSent(true);
      sendMessage(message);
    }}>
      <textarea
        placeholder="Message"
        value={message}
        onChange={e => setMessage(e.target.value)}
      />
      <button type="submit">Send</button>
    </form>
  );
}

function sendMessage(message) {
  // ...
}

Ecco cosa succede quando fai click sul pulsante:

  1. Viene eseguito il gestore di eventi onSubmit.
  2. setIsSent(true) assegnat isSent a true e mette in coda un nuovo render.
  3. React re-renderizza il componente in base al nuovo valore di isSent.

Esaminiamo più da vicino la relazione tra lo stato e il renderizzato.

La renderizzazione scatta un’istantanea nel tempo

“Renderizzare” significa che React chiama il componente, che è una funzione. Il JSX che restituisce tale funzione è una istantanea della UI nel tempo. Le props, i gestori di eventi e le variabili locali sono stati calcolati utilizzando il suo stato al momento del renderizzato.

A differenza di una fotografia o di un fotogramma di un film, l‘“istantanea” della UI che viene restituita è interattiva. Include la logica, come i gestori di eventi che specificano cosa succede in risposta agli input. React aggiorna lo schermo in base a questa istantanea e collega i gestori di eventi. Di conseguenza, la pressione di un pulsante attiverà il gestore di click dal JSX.

Quando React re-renderizza un componente:

  1. React chiama di nuovo la tua funzione.
  2. La tua funzione restituisce una nuova istantanea JSX.
  3. React aggiorna quindi la schermata in modo che corrisponda all’istantanea restituita.
  1. React esegue la funzione
  2. Calcola la istantanea
  3. Aggiorna l'albero del DOM

Illustrato da Rachel Lee Nabors

Come memoria di un componente, lo stato non è come una normale variabile che scompare dopo che la tua funzione restituisce un valore. Lo stato “vive” nello stesso React—come se si trattasse di uno scaffale!—fuori dalla tua funzione. Quando React chiama il tuo componente, fornisce un’istantanea della UI per quel particolare renderizzato. Il tuo componente restituisce un’istantanea della UI con un nuovo set di props e gestori di eventi nel suo JSX, tutti calcolati usando i valori dello stato di quel renderizzato.

  1. Tu dici a React di aggiornare lo state
  2. React aggiorna il valore dello stato
  3. React passa un'istantanea del valore dello stato al componente

Illustrato da Rachel Lee Nabors

Ecco qui un piccolo esperimento per mostrarti come questo funziona. In questo esempio, potresti aspettarti che, cliccando il bottone “+3”, il contatore venga incrementato tre volte, perché viene richiamato setNumber(number + 1) tre volte.

Guarda cosa succede quando fai click sul bottone “+3”:

import { useState } from 'react';

export default function Counter() {
  const [number, setNumber] = useState(0);

  return (
    <>
      <h1>{number}</h1>
      <button onClick={() => {
        setNumber(number + 1);
        setNumber(number + 1);
        setNumber(number + 1);
      }}>+3</button>
    </>
  )
}

Osserva che number viene incrementato solo una volta per click!

L’assegnazione dello stato cambia solo per il prossimo renderizzato. Durante il primo renderizzato, number era 0. È per questo che, nel gestore onClick di quel renderizzato, il valore di number è ancora 0 anche dopo che stato richiamato setNumber(number + 1):

<button onClick={() => {
setNumber(number + 1);
setNumber(number + 1);
setNumber(number + 1);
}}>+3</button>

Questo è ciò che il gestore del click per questo pulsante dice a React di fare:

  1. setNumber(number + 1): number è 0 quindi setNumber(0 + 1).
    • React si prepara per cambiare number a 1 nella prossima renderizzazione.
  2. setNumber(number + 1): number è 0 quindi setNumber(0 + 1).
    • React si prepara per cambiare number a 1 nella prossima renderizzazione.
  3. setNumber(number + 1): number è 0 quindi setNumber(0 + 1).
    • React si prepara per cambiare number a 1 nella prossima renderizzazione.

Anche se chiami setNumber(number + 1) tre volte, nel gestore di eventi di questa renderizzazione number è sempre 0, quindi stai assegnando lo stato a 1 per tre volte. Questo è il motivo per cui, dopo che il tuo gestore di eventi ha finito, React re-renderizza il componente con number uguale a 1 piuttosto che 3.

Puoi anche visualizzarlo sostituendo mentalmente le variabili di stato con i loro valori nel tuo codice. Poiché il valore della variabile di stato number è 0 per questo renderizzato, il suo gestore di eventi si presenta in questo modo:

<button onClick={() => {
setNumber(0 + 1);
setNumber(0 + 1);
setNumber(0 + 1);
}}>+3</button>

Per il prossimo renderizzato, number sarà 1, quindi il gestore del click di quel prossimo renderizzato avrà l’aspetto seguente:

<button onClick={() => {
setNumber(1 + 1);
setNumber(1 + 1);
setNumber(1 + 1);
}}>+3</button>

Per questo, cliccando di nuovo il bottone, il contatore viene impostato a 2, successivamente nel prossimo click a 3 e cosi via.

Lo stato nel tempo

Bene, questo è stato divertente. Prova a indovinare che cosa mostrerà l’alert al click di questo bottone.

import { useState } from 'react';

export default function Counter() {
  const [number, setNumber] = useState(0);

  return (
    <>
      <h1>{number}</h1>
      <button onClick={() => {
        setNumber(number + 5);
        alert(number);
      }}>+5</button>
    </>
  )
}

Se utilizzi il metodo si sostituzione di prima, puoi intuire che l’alert mostra “0”:

setNumber(0 + 5);
alert(0);

Ma cosa succede se imposti un timer per l’alert, in modo che si attivi dopo che il componente ha re-renderizzato? Mostrerà “0” o “5”? Indovina!

import { useState } from 'react';

export default function Counter() {
  const [number, setNumber] = useState(0);

  return (
    <>
      <h1>{number}</h1>
      <button onClick={() => {
        setNumber(number + 5);
        setTimeout(() => {
          alert(number);
        }, 3000);
      }}>+5</button>
    </>
  )
}

Sorpreso? Se hai utilizzato il metodo di sostituzione, puoi vedere l‘“istantanea” del valore dello stato passato all’alert.

setNumber(0 + 5);
setTimeout(() => {
alert(0);
}, 3000);

Il valore dello stato memorizzato in React potrebbe essere cambiato al momento in cui si esegue l’alert, ma questo è stato pianificato utilizzando un’istantanea dello stato nel momento in cui l’utente ha interagito con il esso!

Il valore di una variabile di stato non cambia mai all’interno di un renderizzamento, anche se il codice del suo gestore di eventi è asincrono. Dentro l’onClick di quel render, il valore di number continua a essere 0 anche dopo che setNumber(number + 5) è stato eseguito. Il suo valore è stato “fissato” quando React ha “scattato l’istantanea” della UI chiamanto il tuo componente.

Ecco un esempio di come questo rende i gestori di eventi meno inclini a errori di sincronizzazione. Di seguito è riportato un formulario che invia un messaggio con un ritardo di cinque secondi. Immagina questo scenario:

  1. Premi il pulsante “Send”, inviando “Hello” ad Alice.
  2. Prima dello scadere dei cinque secondi, cambia il valore del campo “To” in “Bob”.

Cosa ti aspetti che mostri l’alert? Mostrerà “You said Hello to Alice”? Oppure “You said Hello to Bob”? Fai una previsione basandoti su che ciò che hai imparato e dopo provalo:

import { useState } from 'react';

export default function Form() {
  const [to, setTo] = useState('Alice');
  const [message, setMessage] = useState('Hello');

  function handleSubmit(e) {
    e.preventDefault();
    setTimeout(() => {
      alert(`You said ${message} to ${to}`);
    }, 5000);
  }

  return (
    <form onSubmit={handleSubmit}>
      <label>
        To:{' '}
        <select
          value={to}
          onChange={e => setTo(e.target.value)}>
          <option value="Alice">Alice</option>
          <option value="Bob">Bob</option>
        </select>
      </label>
      <textarea
        placeholder="Message"
        value={message}
        onChange={e => setMessage(e.target.value)}
      />
      <button type="submit">Send</button>
    </form>
  );
}

React mantiene i valori di stato “fissi” all’interno dei gestori di eventi di un renderizzato. Non c’è bisogno di preoccuparsi se lo stato è cambiato durante l’esecuzione del codice.

Ma cosa succede se vuoi leggere lo stato più recente prima di una nuova renderizzazione? In questo caso, si dovrebbe utilizzare una funzione di aggiornamento dello stato, descritta pagina successiva!

Riepilogo

  • L’assegnazione dello stato richiede un nuovo renderizzamento.
  • React memorizza lo stato al di fuori del tuo componente, come se fosse su uno scaffale.
  • Quando chiami useState, React ti fornisce un’istantanea dello stato per quella renderizzazione.
  • Le variabili e i gestori di eventi non “sopravvivono” ai re-renderizzamenti. Ongi renderizzamento ha i propri gestori di eventi.
  • Ogni renderizzamento (e le funzioni al suo interno) “vedranno” sempre l’istantanea dello stato che React ha dato in quel renderizzamento.
  • Puoi sostituire mentalmente lo stato nei gestori di eventi, in modo simile a a come pensi nel JSX renderizzato.
  • I gestori di eventi creati in passato hanno il valore di stato del renderizzato in cui sono stati creati

Sfida 1 di 1:
Implementa un semaforo

Ecco un componente luminoso per le strisce pedonali che cambia quando viene premuto il bottone:

import { useState } from 'react';

export default function TrafficLight() {
  const [walk, setWalk] = useState(true);

  function handleClick() {
    setWalk(!walk);
  }

  return (
    <>
      <button onClick={handleClick}>
        Change to {walk ? 'Stop' : 'Walk'}
      </button>
      <h1 style={{
        color: walk ? 'darkgreen' : 'darkred'
      }}>
        {walk ? 'Walk' : 'Stop'}
      </h1>
    </>
  );
}

Aggiungi un alert al gestore di click. Quando la luce è verde e dice “Walk”, cliccando il bottone dovrebbe dire “Stop is next”. Quando la luce è rossa e dice “Stop”, cliccando il bottone dovrebbe dire “Walk is next”.

C’è qualche differenza se l’alert viene impostato prima o dopo la chiamata a setWalk? Does it make a difference whether you put the alert before or after the setWalk call?