Reagire all'input con lo State

Nota bene

Questa pagina è stata tradotta automaticamente e supervisionata da un maintainer. Un’ulteriore revisione da parte della community sarebbe comunque utile. Migliora questa traduzione.

React offre un modo dichiarativo di manipolare la UI. Invece di manipolare direttamente i singoli pezzi della UI, descrivi i diversi stati visuali in cui il tuo componente può trovarsi e passi da uno all’altro in risposta all’input dell’utente. È simile a come i designer pensano la UI.

Imparerai

  • In cosa la programmazione UI dichiarativa differisce da quella imperativa
  • Come elencare i diversi stati visuali in cui il tuo componente può trovarsi
  • Come innescare i passaggi tra i diversi stati visuali dal codice

Come la UI dichiarativa si confronta con quella imperativa

Quando progetti interazioni UI, probabilmente pensi a come la UI cambia in risposta alle azioni dell’utente. Considera un form che permette all’utente di inviare una risposta:

  • Quando digiti qualcosa nel form, il pulsante “Submit” viene abilitato.
  • Quando premi “Submit”, sia il form che il pulsante vengono disabilitati e compare uno spinner.
  • Se la richiesta di rete ha successo, il form viene nascosto e compare il messaggio “Thank you”.
  • Se la richiesta di rete fallisce, compare un messaggio di errore e il form viene abilitato di nuovo.

Nella programmazione imperativa, quanto sopra corrisponde direttamente a come implementi l’interazione. Devi scrivere le istruzioni esatte per manipolare la UI a seconda di cosa è appena successo. Ecco un altro modo di pensarci: immagina di sederti accanto a qualcuno in macchina e dirgli turno per turno dove andare.

In un'auto guidata da una persona dall'aria ansiosa che rappresenta JavaScript, un passeggero ordina al conducente di eseguire una sequenza complicata di navigazioni turno per turno.

Illustrato da Rachel Lee Nabors

Non sanno dove vuoi andare, seguono solo i tuoi comandi. (E se sbagli le indicazioni, finisci nel posto sbagliato!) Si chiama imperativa perché devi “comandare” ogni elemento, dallo spinner al pulsante, dicendo al computer come aggiornare la UI.

In questo esempio di programmazione UI imperativa, il form è costruito senza React. Usa solo il DOM del browser:

async function handleFormSubmit(e) {
  e.preventDefault();
  disable(textarea);
  disable(button);
  show(loadingMessage);
  hide(errorMessage);
  try {
    await submitForm(textarea.value);
    show(successMessage);
    hide(form);
  } catch (err) {
    show(errorMessage);
    errorMessage.textContent = err.message;
  } finally {
    hide(loadingMessage);
    enable(textarea);
    enable(button);
  }
}

function handleTextareaChange() {
  if (textarea.value.length === 0) {
    disable(button);
  } else {
    enable(button);
  }
}

function hide(el) {
  el.style.display = 'none';
}

function show(el) {
  el.style.display = '';
}

function enable(el) {
  el.disabled = false;
}

function disable(el) {
  el.disabled = true;
}

function submitForm(answer) {
  // Pretend it's hitting the network.
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (answer.toLowerCase() === 'istanbul') {
        resolve();
      } else {
        reject(new Error('Good guess but a wrong answer. Try again!'));
      }
    }, 1500);
  });
}

let form = document.getElementById('form');
let textarea = document.getElementById('textarea');
let button = document.getElementById('button');
let loadingMessage = document.getElementById('loading');
let errorMessage = document.getElementById('error');
let successMessage = document.getElementById('success');
form.onsubmit = handleFormSubmit;
textarea.oninput = handleTextareaChange;

Manipolare la UI in modo imperativo funziona abbastanza bene per esempi isolati, ma diventa esponenzialmente più difficile da gestire in sistemi più complessi. Immagina di aggiornare una pagina piena di form diversi come questo. Aggiungere un nuovo elemento UI o una nuova interazione richiederebbe di controllare attentamente tutto il codice esistente per assicurarti di non aver introdotto un bug (ad esempio, dimenticare di mostrare o nascondere qualcosa).

React è stato creato per risolvere questo problema.

In React, non manipoli direttamente la UI — cioè non abiliti, disabiliti, mostri o nascondi i componenti direttamente. Invece, dichiari cosa vuoi mostrare e React capisce come aggiornare la UI. Pensa di salire su un taxi e dire al conducente dove vuoi andare invece di dirgli esattamente dove girare. È compito del conducente portarti lì, e potrebbe anche conoscere scorciatoie che non avevi considerato!

In un'auto guidata da React, un passeggero chiede di essere portato in un luogo specifico sulla mappa. React capisce come farlo.

Illustrato da Rachel Lee Nabors

Pensare alla UI in modo dichiarativo

Hai visto sopra come implementare un form in modo imperativo. Per capire meglio come pensare in React, riimplementerai questa UI in React di seguito:

  1. Identifica i diversi stati visuali del tuo componente
  2. Determina cosa innesca quei cambiamenti di state
  3. Rappresenta lo state in memoria usando useState
  4. Rimuovi tutte le variabili di state non essenziali
  5. Collega i gestori di eventi per impostare lo state

Step 1: Identifica i diversi stati visuali del tuo componente

In informatica, potresti sentir parlare di una “macchina a stati” che si trova in uno di diversi “stati”. Se lavori con un designer, potresti aver visto mockup per diversi “stati visuali”. React si colloca all’incrocio tra design e informatica, quindi entrambe queste idee sono fonti di ispirazione.

Per prima cosa, devi visualizzare tutti i diversi “stati” della UI che l’utente potrebbe vedere:

  • Empty: Il form ha un pulsante “Submit” disabilitato.
  • Typing: Il form ha un pulsante “Submit” abilitato.
  • Submitting: Il form è completamente disabilitato. Viene mostrato uno spinner.
  • Success: Al posto del form viene mostrato il messaggio “Thank you”.
  • Error: Come in Typing, ma con un messaggio di errore aggiuntivo.

Proprio come un designer, vorrai creare “mock” o “mockup” per i diversi stati prima di aggiungere la logica. Ad esempio, ecco un mock solo per la parte visiva del form. Questo mock è controllato da una prop chiamata status con un valore predefinito di 'empty':

export default function Form({
  status = 'empty'
}) {
  if (status === 'success') {
    return <h1>That's right!</h1>
  }
  return (
    <>
      <h2>City quiz</h2>
      <p>
        In which city is there a billboard that turns air into drinkable water?
      </p>
      <form>
        <textarea />
        <br />
        <button>
          Submit
        </button>
      </form>
    </>
  )
}

Potresti chiamare quella prop come preferisci, il nome non è importante. Prova a modificare status = 'empty' in status = 'success' per vedere comparire il messaggio di successo. Creare mock ti permette di iterare rapidamente sulla UI prima di collegare qualsiasi logica. Ecco un prototipo più completo dello stesso componente, ancora “controllato” dalla prop status:

export default function Form({
  // Try 'submitting', 'error', 'success':
  status = 'empty'
}) {
  if (status === 'success') {
    return <h1>That's right!</h1>
  }
  return (
    <>
      <h2>City quiz</h2>
      <p>
        In which city is there a billboard that turns air into drinkable water?
      </p>
      <form>
        <textarea disabled={
          status === 'submitting'
        } />
        <br />
        <button disabled={
          status === 'empty' ||
          status === 'submitting'
        }>
          Submit
        </button>
        {status === 'error' &&
          <p className="Error">
            Good guess but a wrong answer. Try again!
          </p>
        }
      </form>
      </>
  );
}

Approfondimento

Mostrare molti stati visuali contemporaneamente

Se un componente ha molti stati visuali, può essere comodo mostrarli tutti in una sola pagina:

import Form from './Form.js';

let statuses = [
  'empty',
  'typing',
  'submitting',
  'success',
  'error',
];

export default function App() {
  return (
    <>
      {statuses.map(status => (
        <section key={status}>
          <h4>Form ({status}):</h4>
          <Form status={status} />
        </section>
      ))}
    </>
  );
}

Pagine come questa sono spesso chiamate “living styleguide” o “storybook”.

Step 2: Determina cosa innesca quei cambiamenti di state

Puoi innescare aggiornamenti dello state in risposta a due tipi di input:

  • Input umani, come cliccare un pulsante, digitare in un campo, navigare un link.
  • Input del computer, come l’arrivo di una risposta di rete, il completamento di un timeout, il caricamento di un’immagine.
Un dito.
Input umani
Uno e zero.
Input del computer

Illustrato da Rachel Lee Nabors

In entrambi i casi, devi impostare le variabili di state per aggiornare la UI. Per il form che stai sviluppando, dovrai cambiare lo state in risposta a diversi input:

  • Cambiare l’input di testo (umano) dovrebbe passare dallo stato Empty allo stato Typing o viceversa, a seconda che la casella di testo sia vuota o meno.
  • Cliccare il pulsante Submit (umano) dovrebbe passare allo stato Submitting.
  • Risposta di rete riuscita (computer) dovrebbe passare allo stato Success.
  • Risposta di rete fallita (computer) dovrebbe passare allo stato Error con il messaggio di errore corrispondente.

Nota bene

Nota che gli input umani spesso richiedono gestori di eventi!

Per aiutarti a visualizzare questo flusso, prova a disegnare ogni stato visivo su un foglio come un cerchio etichettato, e ogni cambiamento tra due stati come una freccia. Puoi abbozzare molti flussi in questo modo e individuare bug molto prima dell’implementazione.

Diagramma di flusso che si muove da sinistra a destra con 5 nodi. Il primo nodo etichettato 'empty' ha un arco etichettato 'start typing' collegato a un nodo etichettato 'typing'. Quel nodo ha un arco etichettato 'press submit' collegato a un nodo etichettato 'submitting', che ha due archi. L'arco sinistro è etichettato 'network error' e collega a un nodo etichettato 'error'. L'arco destro è etichettato 'network success' e collega a un nodo etichettato 'success'.
Diagramma di flusso che si muove da sinistra a destra con 5 nodi. Il primo nodo etichettato 'empty' ha un arco etichettato 'start typing' collegato a un nodo etichettato 'typing'. Quel nodo ha un arco etichettato 'press submit' collegato a un nodo etichettato 'submitting', che ha due archi. L'arco sinistro è etichettato 'network error' e collega a un nodo etichettato 'error'. L'arco destro è etichettato 'network success' e collega a un nodo etichettato 'success'.

Stati del form

Step 3: Rappresenta lo state in memoria con useState

Poi dovrai rappresentare gli stati visuali del tuo componente in memoria con useState. La semplicità è fondamentale: ogni pezzo di state è un “pezzo mobile”, e vuoi il minor numero possibile di “pezzi mobili”. Più complessità significa più bug!

Inizia con lo state che deve assolutamente esserci. Ad esempio, dovrai memorizzare la answer per l’input e l’error (se esiste) per memorizzare l’ultimo errore:

const [answer, setAnswer] = useState('');
const [error, setError] = useState(null);

Poi, avrai bisogno di una variabile di state che rappresenti quale degli stati visuali vuoi mostrare. Di solito c’è più di un modo per rappresentarlo in memoria, quindi dovrai sperimentare.

Se fai fatica a pensare subito al modo migliore, inizia aggiungendo abbastanza state da essere sicuro che tutti i possibili stati visuali siano coperti:

const [isEmpty, setIsEmpty] = useState(true);
const [isTyping, setIsTyping] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
const [isError, setIsError] = useState(false);

La tua prima idea probabilmente non sarà la migliore, ma va bene — rifattorizzare lo state fa parte del processo!

Step 4: Rimuovi tutte le variabili di state non essenziali

Vuoi evitare duplicazioni nel contenuto dello state così da tracciare solo ciò che è essenziale. Dedicare un po’ di tempo a rifattorizzare la struttura dello state renderà i tuoi componenti più facili da capire, ridurrà la duplicazione ed eviterà significati involontari. Il tuo obiettivo è prevenire i casi in cui lo state in memoria non rappresenta alcuna UI valida che vorresti mostrare a un utente. (Ad esempio, non vorresti mai mostrare un messaggio di errore e disabilitare l’input contemporaneamente, altrimenti l’utente non potrà correggere l’errore!)

Ecco alcune domande che puoi fare sulle tue variabili di state:

  • Questo state causa un paradosso? Ad esempio, isTyping e isSubmitting non possono essere entrambi true. Un paradosso di solito significa che lo state non è abbastanza vincolato. Ci sono quattro possibili combinazioni di due booleani, ma solo tre corrispondono a stati validi. Per rimuovere lo state “impossibile”, puoi combinarli in un status che deve essere uno di tre valori: 'typing', 'submitting' o 'success'.
  • Le stesse informazioni sono già disponibili in un’altra variabile di state? Un altro paradosso: isEmpty e isTyping non possono essere entrambi true contemporaneamente. Rendendoli variabili di state separate, rischi che si desincronizzino e causino bug. Fortunatamente, puoi rimuovere isEmpty e controllare invece answer.length === 0.
  • Puoi ottenere le stesse informazioni dall’inverso di un’altra variabile di state? isError non serve perché puoi controllare error !== null.

Dopo questa pulizia, ti restano 3 (da 7!) variabili di state essenziali:

const [answer, setAnswer] = useState('');
const [error, setError] = useState(null);
const [status, setStatus] = useState('typing'); // 'typing', 'submitting', or 'success'

Sai che sono essenziali, perché non puoi rimuoverne nessuna senza rompere la funzionalità.

Approfondimento

Eliminare stati “impossibili” con un reducer

Queste tre variabili sono una rappresentazione abbastanza buona dello state di questo form. Tuttavia, ci sono ancora alcuni stati intermedi che non hanno pieno senso. Ad esempio, un error non nullo non ha senso quando status è 'success'. Per modellare lo state in modo più preciso, puoi estrarlo in un reducer. I reducer ti permettono di unificare più variabili di state in un singolo oggetto e consolidare tutta la logica correlata!

Step 5: Collega i gestori di eventi per impostare lo state

Infine, crea gestori di eventi che aggiornano lo state. Di seguito c’è il form finale, con tutti i gestori di eventi collegati:

import { useState } from 'react';

export default function Form() {
  const [answer, setAnswer] = useState('');
  const [error, setError] = useState(null);
  const [status, setStatus] = useState('typing');

  if (status === 'success') {
    return <h1>That's right!</h1>
  }

  async function handleSubmit(e) {
    e.preventDefault();
    setStatus('submitting');
    try {
      await submitForm(answer);
      setStatus('success');
    } catch (err) {
      setStatus('typing');
      setError(err);
    }
  }

  function handleTextareaChange(e) {
    setAnswer(e.target.value);
  }

  return (
    <>
      <h2>City quiz</h2>
      <p>
        In which city is there a billboard that turns air into drinkable water?
      </p>
      <form onSubmit={handleSubmit}>
        <textarea
          value={answer}
          onChange={handleTextareaChange}
          disabled={status === 'submitting'}
        />
        <br />
        <button disabled={
          answer.length === 0 ||
          status === 'submitting'
        }>
          Submit
        </button>
        {error !== null &&
          <p className="Error">
            {error.message}
          </p>
        }
      </form>
    </>
  );
}

function submitForm(answer) {
  // Pretend it's hitting the network.
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      let shouldError = answer.toLowerCase() !== 'lima'
      if (shouldError) {
        reject(new Error('Good guess but a wrong answer. Try again!'));
      } else {
        resolve();
      }
    }, 1500);
  });
}

Anche se questo codice è più lungo dell’esempio imperativo originale, è molto meno fragile. Esprimere tutte le interazioni come cambiamenti di state ti permette di introdurre in seguito nuovi stati visuali senza rompere quelli esistenti. Ti permette anche di cambiare cosa deve essere mostrato in ogni stato visivo senza modificare la logica dell’interazione stessa.

Riepilogo

  • La programmazione dichiarativa significa descrivere la UI per ogni stato visivo piuttosto che microgestire la UI (imperativo).
  • Quando sviluppi un componente:
    1. Identifica tutti i suoi stati visuali.
    2. Determina gli input umani e del computer che innescano i cambiamenti di state.
    3. Modella lo state con useState.
    4. Rimuovi lo state non essenziale per evitare bug e paradossi.
    5. Collega i gestori di eventi per impostare lo state.

Sfida 1 di 3:
Aggiungere e rimuovere una classe CSS

Fai in modo che cliccare sull’immagine rimuova la classe CSS background--active dal <div> esterno, ma aggiunga la classe picture--active all’<img>. Cliccare di nuovo sullo sfondo dovrebbe ripristinare le classi CSS originali.

Visivamente, dovresti aspettarti che cliccare sull’immagine rimuova lo sfondo viola e evidenzi il bordo dell’immagine. Cliccare fuori dall’immagine evidenzia lo sfondo, ma rimuove l’evidenziazione del bordo dell’immagine.

export default function Picture() {
  return (
    <div className="background background--active">
      <img
        className="picture"
        alt="Rainbow houses in Kampung Pelangi, Indonesia"
        src="https://react.dev/images/docs/scientists/5qwVYb1.jpeg"
      />
    </div>
  );
}