Lo State come un'Istantanea

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

Imparerai

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

Come l’assegnazione dello state innesca 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 state si richiede una nuova ri-renderizzazione da React. Questo significa che per far reagire l’interfaccia al evento, è necessario aggiornare lo state.

In questo esempio, quando premi “send”, setIsSent(true) dice a React di ri-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) assegna 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 state 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 è come un’istantanea della UI nel tempo. Le props, i gestori di eventi e le variabili locali sono stati calcolati utilizzando il suo state al momento della renderizzazione.

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 tuo 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 l'istantanea
  3. Aggiorna l'albero del DOM

Illustrato da Rachel Lee Nabors

Come memoria di un componente, lo state non è come una normale variabile che scompare dopo che la tua funzione restituisce un valore. Lo state “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 state
  3. React passa un'istantanea del valore dello state 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 chiamato 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 state cambia solo per la prossima renderizzazione . Durante la prima renderizzazione, number era 0. È per questo che, nel gestore onClick di quella renderizzazione, il valore di number è ancora 0 anche dopo che stato chiamato 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 hai chiamato setNumber(number + 1) tre volte, nel gestore di eventi di questa renderizzazione number è sempre 0, quindi stai assegnando lo state a 1 per tre volte. Questo è il motivo per cui, dopo che il tuo gestore di eventi ha finito, React ri-renderizza il componente con number uguale a 1 piuttosto che 3.

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

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

Per la prossima renderizzazione, number sarà 1, quindi il gestore del click di quella prossima renderizzazione avrà il seguente aspetto:

<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 state 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 di 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 viene ri-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 state passato all’alert.

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

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

Il valore di una variabile di state non cambia mai all’interno di una renderizzazione, anche se il codice del suo gestore di eventi è asincrono. Dentro l’onClick di quella renderizzazione, 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 chiamando 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 state “fissi” all’interno dei gestori di eventi di una renderizzazione. Non c’è bisogno di preoccuparsi se lo state è cambiato durante l’esecuzione del codice.

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

Riepilogo

  • L’assegnazione dello state richiede una nuova renderizzazione.
  • React memorizza lo state al di fuori del tuo componente, come se fosse su uno scaffale.
  • Quando chiami useState, React ti fornisce un’istantanea dello state per quella renderizzazione.
  • Le variabili e i gestori di eventi non “sopravvivono” alle ri-renderizzazioni. Ogni renderizzazione ha i propri gestori di eventi.
  • Ogni renderizzazione (e le funzioni al suo interno) “vedrà” sempre l’istantanea dello state che React ha dato in quella renderizzazione.
  • Puoi sostituire mentalmente lo state 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 state della renderizzazione 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?