State: La Memoria di un Componente

Spesso i componenti devono modificare ciò che c’è sullo schermo in seguito a un’interazione. Scrivere nel form deve aggiornare l’input, cliccare “avanti” su un carosello deve cambiare l’immagine mostrata, cliccare “acquista” inserisce un prodotto nel carrello. I componenti devono “ricordare” le cose: l’attuale valore dell’input, l’attuale immagine, il carrello. In React, questo specifico tipo di memoria è detto state.

Imparerai

  • Come aggiungere una variabile state con l’Hook useState
  • Quale coppia di valori restituisce l’Hook useState
  • Come aggiungere più di una variabile state
  • Perché lo state si definisce locale

Quando una variabile normale non è sufficiente

Ecco un componente che renderizza l’immagine di una scultura. Cliccare sul pulsante “Next” dovrebbe mostrare la scultura successiva, modificando l’index a 1, poi 2, e così via. Tuttavia, questo non funzionerà (puoi provare!):

import { sculptureList } from './data.js';

export default function Gallery() {
  let index = 0;

  function handleClick() {
    index = index + 1;
  }

  let sculpture = sculptureList[index];
  return (
    <>
      <button onClick={handleClick}>
        Next
      </button>
      <h2>
        <i>{sculpture.name} </i> 
        by {sculpture.artist}
      </h2>
      <h3>  
        ({index + 1} of {sculptureList.length})
      </h3>
      <img 
        src={sculpture.url} 
        alt={sculpture.alt}
      />
      <p>
        {sculpture.description}
      </p>
    </>
  );
}

L’event handler handleClick sta aggiornando una variabile locale, index. Ma due cose impediscono alla modifica di essere visibile:

  1. Le variabili locali non persistono tra le renderizzazioni. Quando React renderizza questo componente una seconda volta, lo renderizza da zero e non considera eventuali modifiche alle variabili locali.
  2. Le modifiche alle variabili locali non triggerano il render. React non si accorge di dover renderizzare un’altra volta il componente con i dati nuovi.

Per aggiornare un componente con nuovi dati, devono accadere due cose:

  1. Conservare i dati tra le renderizzazioni.
  2. Triggerare React per renderizzare il componente con nuovi dati (ri-renderizzazione).

L’Hook useState offre queste due cose:

  1. Una variabile state per conservare i dati tra le renderizzazioni.
  2. Una funzione state setter per aggiornare la variabile e triggerare React per renderizzare di nuovo il componente.

Aggiungere una variabile state

Per aggiungere una variabile state, importa useState da React in cima al file:

import { useState } from 'react';

Poi, sostituisci questa riga:

let index = 0;

con

const [index, setIndex] = useState(0);

index è una variabile state e setIndex è la funzione setter.

La sintassi [ e ] è chiamata destrutturazione di un array e ti consente di leggere i valori di un array. L’array ritornato da useState ha sempre esattamente due elementi.

Ecco come funzionano insieme in handleClick:

function handleClick() {
setIndex(index + 1);
}

Ora cliccare sul pulsante “Next” modifica la scultura corrente:

import { useState } from 'react';
import { sculptureList } from './data.js';

export default function Gallery() {
  const [index, setIndex] = useState(0);

  function handleClick() {
    setIndex(index + 1);
  }

  let sculpture = sculptureList[index];
  return (
    <>
      <button onClick={handleClick}>
        Next
      </button>
      <h2>
        <i>{sculpture.name} </i> 
        by {sculpture.artist}
      </h2>
      <h3>  
        ({index + 1} of {sculptureList.length})
      </h3>
      <img 
        src={sculpture.url} 
        alt={sculpture.alt}
      />
      <p>
        {sculpture.description}
      </p>
    </>
  );
}

Ecco il tuo primo Hook

In React, useState, così come qualsiasi altra funzione che inizia per ”use”, è chiamata Hook.

Gli Hook sono funzioni speciali disponibili solo durante la fase di renderizzazione di React (che vedremo nel dettaglio nella pagina successiva). Ti consentono di “agganciarti” a diverse funzionalità di React.

Lo State è solo una di queste funzionalità, ma incontrerai gli altri Hook più avanti.

Insidia

Gli Hook (funzioni che iniziano per use) possono essere chiamati solo al livello superiore del tuo componente o dei tuoi Hook. Non è possibile chiamare gli Hook all’interno di condizioni, cicli o altre funzioni nidificate. Gli Hook sono funzioni, ma è di aiuto considerarli come dichiarazioni incondizionate sulle necessità del tuo componente. “Usi” le funzionalità di React in cima al tuo componente in modo simile a come “importi” i moduli in cima al tuo file.

Anatomia di useState

Quando chiami useState, stai dicendo a React che vuoi che questo componente ricordi qualcosa:

const [index, setIndex] = useState(0);

In questo caso, vuoi che React ricordi index.

Nota

La convenzione prevede di chiamare questa coppia come const [something, setSomething]. Potresti darle il nome che preferisci, ma le convenzioni facilitano la comprensione tra i vari progetti.

L’unico argomento di useState è il valore iniziale della tua variabile state. In questo esempio, il valore iniziale di index è impostato a 0 con useState(0).

Ogni volta che il tuo componente renderizza, useState ti da un array contenente due valori:

  1. La variabile state (index) con il valore che hai memorizzato.
  2. La funzione state setter (setIndex) che può aggiornare la variabile state e triggerare React a renderizzare di nuovo il componente.

Ecco come questo accade in azione:

const [index, setIndex] = useState(0);
  1. Il tuo componente renderizza per la prima volta. Poiché hai passato 0 a useState come valore iniziale di index, questo ritornerà [0, setIndex]. React ricorda che 0 è l’ultimo valore dello state.
  2. Aggiorni lo state. Quando un utente clicca il pulsante, questo chiama setIndex(index + 1). index è 0, quindi diventa setIndex(1). Questo dice a React di ricordare che index ora è 1 e di triggerare un’altra renderizzazione.
  3. La seconda renderizzazione del tuo componente. React ancora vede useState(0), ma poiché React ricorda che hai impostato index a 1, ritorna invece [1, setIndex].
  4. E così via!

Dare a un componente più variabili state

Puoi avere quante variabili state di quanti tipi desideri in un componente. Questo componente ha due variabili state, un numero index e un booleano showMore che viene invertito quando clicchi “Show details”:

import { useState } from 'react';
import { sculptureList } from './data.js';

export default function Gallery() {
  const [index, setIndex] = useState(0);
  const [showMore, setShowMore] = useState(false);

  function handleNextClick() {
    setIndex(index + 1);
  }

  function handleMoreClick() {
    setShowMore(!showMore);
  }

  let sculpture = sculptureList[index];
  return (
    <>
      <button onClick={handleNextClick}>
        Next
      </button>
      <h2>
        <i>{sculpture.name} </i> 
        by {sculpture.artist}
      </h2>
      <h3>  
        ({index + 1} of {sculptureList.length})
      </h3>
      <button onClick={handleMoreClick}>
        {showMore ? 'Hide' : 'Show'} details
      </button>
      {showMore && <p>{sculpture.description}</p>}
      <img 
        src={sculpture.url} 
        alt={sculpture.alt}
      />
    </>
  );
}

È una buona idea avere più variabili state se il loro state non è correlato, come index e showMore in questo esempio. Ma se ti rendi conto che modifichi spesso due variabili state allo stesso tempo, potrebbe essere più semplice unirle in una sola. Ad esempio, se hai un form con molti campi, è più conveniente avere una singola variabile state che contiene un oggetto piuttosto che una variabile state per ogni campo. Leggi Scegliere la Struttura dello State per altri consigli.

Approfondimento

Come fa React a sapere quale state ritornare?

Avrai notato che la chiamata useState non riceve alcuna informazione circa quale variabile di state prendere come riferimento. Non viene passato alcun “identificatore” a useState, quindi come fa a sapere quale delle variabili di state restituire? Fa affidamento su qualche magia come il parsing delle tue funzioni? La risposta è no.

Invece, per consentire la loro sintassi concisa, gli Hook si basano su un ordine di chiamata stabile a ogni renderizzazione dello stesso componente. Questo funziona bene in pratica perché se segui la regola sopra (“chiamare solo gli Hook al livello superiore”), gli Hook verranno sempre chiamati nello stesso ordine. Inoltre, un plugin linter rileva la gran parte degli errori.

Internamente, React mantiene un array di coppie di state per ogni componente. Mantiene anche l’indice della coppia corrente, che viene impostato su 0 prima della renderizzazione. Ogni volta che chiami useState, React ti fornisce la coppia di state successiva e incrementa l’indice. Puoi leggere di più su questo meccanismo in React Hooks: Not Magic, Just Arrays.

Questo esempio non utilizza React ma ti dà un’idea di come useState funzioni internamente:

let componentHooks = [];
let currentHookIndex = 0;

// How useState works inside React (simplified).
function useState(initialState) {
  let pair = componentHooks[currentHookIndex];
  if (pair) {
    // This is not the first render,
    // so the state pair already exists.
    // Return it and prepare for next Hook call.
    currentHookIndex++;
    return pair;
  }

  // This is the first time we're rendering,
  // so create a state pair and store it.
  pair = [initialState, setState];

  function setState(nextState) {
    // When the user requests a state change,
    // put the new value into the pair.
    pair[0] = nextState;
    updateDOM();
  }

  // Store the pair for future renders
  // and prepare for the next Hook call.
  componentHooks[currentHookIndex] = pair;
  currentHookIndex++;
  return pair;
}

function Gallery() {
  // Each useState() call will get the next pair.
  const [index, setIndex] = useState(0);
  const [showMore, setShowMore] = useState(false);

  function handleNextClick() {
    setIndex(index + 1);
  }

  function handleMoreClick() {
    setShowMore(!showMore);
  }

  let sculpture = sculptureList[index];
  // This example doesn't use React, so
  // return an output object instead of JSX.
  return {
    onNextClick: handleNextClick,
    onMoreClick: handleMoreClick,
    header: `${sculpture.name} by ${sculpture.artist}`,
    counter: `${index + 1} of ${sculptureList.length}`,
    more: `${showMore ? 'Hide' : 'Show'} details`,
    description: showMore ? sculpture.description : null,
    imageSrc: sculpture.url,
    imageAlt: sculpture.alt
  };
}

function updateDOM() {
  // Reset the current Hook index
  // before rendering the component.
  currentHookIndex = 0;
  let output = Gallery();

  // Update the DOM to match the output.
  // This is the part React does for you.
  nextButton.onclick = output.onNextClick;
  header.textContent = output.header;
  moreButton.onclick = output.onMoreClick;
  moreButton.textContent = output.more;
  image.src = output.imageSrc;
  image.alt = output.imageAlt;
  if (output.description !== null) {
    description.textContent = output.description;
    description.style.display = '';
  } else {
    description.style.display = 'none';
  }
}

let nextButton = document.getElementById('nextButton');
let header = document.getElementById('header');
let moreButton = document.getElementById('moreButton');
let description = document.getElementById('description');
let image = document.getElementById('image');
let sculptureList = [{
  name: 'Homenaje a la Neurocirugía',
  artist: 'Marta Colvin Andrade',
  description: 'Although Colvin is predominantly known for abstract themes that allude to pre-Hispanic symbols, this gigantic sculpture, an homage to neurosurgery, is one of her most recognizable public art pieces.',
  url: 'https://i.imgur.com/Mx7dA2Y.jpg',
  alt: 'A bronze statue of two crossed hands delicately holding a human brain in their fingertips.'  
}, {
  name: 'Floralis Genérica',
  artist: 'Eduardo Catalano',
  description: 'This enormous (75 ft. or 23m) silver flower is located in Buenos Aires. It is designed to move, closing its petals in the evening or when strong winds blow and opening them in the morning.',
  url: 'https://i.imgur.com/ZF6s192m.jpg',
  alt: 'A gigantic metallic flower sculpture with reflective mirror-like petals and strong stamens.'
}, {
  name: 'Eternal Presence',
  artist: 'John Woodrow Wilson',
  description: 'Wilson was known for his preoccupation with equality, social justice, as well as the essential and spiritual qualities of humankind. This massive (7ft. or 2,13m) bronze represents what he described as "a symbolic Black presence infused with a sense of universal humanity."',
  url: 'https://i.imgur.com/aTtVpES.jpg',
  alt: 'The sculpture depicting a human head seems ever-present and solemn. It radiates calm and serenity.'
}, {
  name: 'Moai',
  artist: 'Unknown Artist',
  description: 'Located on the Easter Island, there are 1,000 moai, or extant monumental statues, created by the early Rapa Nui people, which some believe represented deified ancestors.',
  url: 'https://i.imgur.com/RCwLEoQm.jpg',
  alt: 'Three monumental stone busts with the heads that are disproportionately large with somber faces.'
}, {
  name: 'Blue Nana',
  artist: 'Niki de Saint Phalle',
  description: 'The Nanas are triumphant creatures, symbols of femininity and maternity. Initially, Saint Phalle used fabric and found objects for the Nanas, and later on introduced polyester to achieve a more vibrant effect.',
  url: 'https://i.imgur.com/Sd1AgUOm.jpg',
  alt: 'A large mosaic sculpture of a whimsical dancing female figure in a colorful costume emanating joy.'
}, {
  name: 'Ultimate Form',
  artist: 'Barbara Hepworth',
  description: 'This abstract bronze sculpture is a part of The Family of Man series located at Yorkshire Sculpture Park. Hepworth chose not to create literal representations of the world but developed abstract forms inspired by people and landscapes.',
  url: 'https://i.imgur.com/2heNQDcm.jpg',
  alt: 'A tall sculpture made of three elements stacked on each other reminding of a human figure.'
}, {
  name: 'Cavaliere',
  artist: 'Lamidi Olonade Fakeye',
  description: "Descended from four generations of woodcarvers, Fakeye's work blended traditional and contemporary Yoruba themes.",
  url: 'https://i.imgur.com/wIdGuZwm.png',
  alt: 'An intricate wood sculpture of a warrior with a focused face on a horse adorned with patterns.'
}, {
  name: 'Big Bellies',
  artist: 'Alina Szapocznikow',
  description: "Szapocznikow is known for her sculptures of the fragmented body as a metaphor for the fragility and impermanence of youth and beauty. This sculpture depicts two very realistic large bellies stacked on top of each other, each around five feet (1,5m) tall.",
  url: 'https://i.imgur.com/AlHTAdDm.jpg',
  alt: 'The sculpture reminds a cascade of folds, quite different from bellies in classical sculptures.'
}, {
  name: 'Terracotta Army',
  artist: 'Unknown Artist',
  description: 'The Terracotta Army is a collection of terracotta sculptures depicting the armies of Qin Shi Huang, the first Emperor of China. The army consisted of more than 8,000 soldiers, 130 chariots with 520 horses, and 150 cavalry horses.',
  url: 'https://i.imgur.com/HMFmH6m.jpg',
  alt: '12 terracotta sculptures of solemn warriors, each with a unique facial expression and armor.'
}, {
  name: 'Lunar Landscape',
  artist: 'Louise Nevelson',
  description: 'Nevelson was known for scavenging objects from New York City debris, which she would later assemble into monumental constructions. In this one, she used disparate parts like a bedpost, juggling pin, and seat fragment, nailing and gluing them into boxes that reflect the influence of Cubism’s geometric abstraction of space and form.',
  url: 'https://i.imgur.com/rN7hY6om.jpg',
  alt: 'A black matte sculpture where the individual elements are initially indistinguishable.'
}, {
  name: 'Aureole',
  artist: 'Ranjani Shettar',
  description: 'Shettar merges the traditional and the modern, the natural and the industrial. Her art focuses on the relationship between man and nature. Her work was described as compelling both abstractly and figuratively, gravity defying, and a "fine synthesis of unlikely materials."',
  url: 'https://i.imgur.com/okTpbHhm.jpg',
  alt: 'A pale wire-like sculpture mounted on concrete wall and descending on the floor. It appears light.'
}, {
  name: 'Hippos',
  artist: 'Taipei Zoo',
  description: 'The Taipei Zoo commissioned a Hippo Square featuring submerged hippos at play.',
  url: 'https://i.imgur.com/6o5Vuyu.jpg',
  alt: 'A group of bronze hippo sculptures emerging from the sett sidewalk as if they were swimming.'
}];

// Make UI match the initial state.
updateDOM();

Non è necessario che tu lo capisca per utilizzare React, ma potresti trovare utile questo modello mentale.

Lo State è isolato e privato

Lo State è locale rispetto all’istanza di un componente sullo schermo. In altre parole, se renderizzi lo stesso componente due volte, ogni copia avrà uno state completamente isolato! Modificare uno dei due non intaccherà l’altro.

In questo esempio, il componente Gallery di prima viene renderizzato due volte con nessuna modifica apportata alla sua logica. Prova a cliccare sui bottoni all’interno di entrambe le gallerie. Nota come i loro state sono indipendenti:

import Gallery from './Gallery.js';

export default function Page() {
  return (
    <div className="Page">
      <Gallery />
      <Gallery />
    </div>
  );
}

Questo è ciò che rende lo state diverso dalle variabili regolari che potresti dichiarare in cima al tuo modulo. Lo state non è legato a una particolare chiamata di funzione o a un luogo nel codice, ma è “locale” rispetto a un preciso posto sullo schermo. Hai renderizzato due componenti <Gallery />, quindi il loro state viene memorizzato separatamente.

Inoltre nota come il componente Page non “sa” nulla dello state di Gallery ne tantomeno se ne ha uno. A differenza delle prop, lo state è totalmente privato rispetto al componente che lo dichiara. Il componente padre non può modificarlo. Questo ti consente di aggiungere lo state a qualsiasi componente o rimuoverlo, senza avere alcun impatto sul resto dei tuoi componenti.

E se volessi che entrambe le gallerie mantenessero lo state sincronizzato? Il modo corretto per fare questo in React è rimuovere lo state dai componenti figli e aggiungerlo al padre comune più vicino. Le prossime pagine si focalizzeranno sull’organizzazione dello state in un singolo componente, ma torneremo su questo argomento in Condividere lo State tra Componenti.

Riepilogo

  • Usa una variabile state quando un componente deve “ricordare” qualche informazione tra le renderizzazioni.
  • Le variabili state sono dichiarate chiamando l’Hook useState.
  • Gli Hook sono funzioni speciali che iniziano per use. Ti consentono di “agganciarti” alle funzionalità di React come lo state.
  • Gli Hook potrebbero ricordati gli import: devono essere chiamati incondizionatamente. Chiamare gli Hook, incluso useState, è valido solo al livello superiore del componente o di un altro Hook.
  • L’Hook useState ritorna una coppia di valori: lo state attuale e la funzione per aggiornarlo.
  • Puoi avere più di una variabile state. Internamente, React le distingue in base all’ordine.
  • Lo state è privato rispetto al componente. Se quest’ultimo lo renderizzi in due posti, ogni copia avrà il suo state.

Quando, arrivato all’ultima scultura, clicchi su “Next”, il codice va in errore. Correggi la logica per prevenire l’errore. Potresti aggiungere della logica in più nell’event handler o disabilitare il pulsante quando l’azione non è possibile.

Una volta risolto l’errore, aggiungi un pulsante “Previous” che mostra la scultura precedente. Non dovrebbe andare in errore quando si è sulla prima scultura.

import { useState } from 'react';
import { sculptureList } from './data.js';

export default function Gallery() {
  const [index, setIndex] = useState(0);
  const [showMore, setShowMore] = useState(false);

  function handleNextClick() {
    setIndex(index + 1);
  }

  function handleMoreClick() {
    setShowMore(!showMore);
  }

  let sculpture = sculptureList[index];
  return (
    <>
      <button onClick={handleNextClick}>
        Next
      </button>
      <h2>
        <i>{sculpture.name} </i> 
        by {sculpture.artist}
      </h2>
      <h3>  
        ({index + 1} of {sculptureList.length})
      </h3>
      <button onClick={handleMoreClick}>
        {showMore ? 'Hide' : 'Show'} details
      </button>
      {showMore && <p>{sculpture.description}</p>}
      <img 
        src={sculpture.url} 
        alt={sculpture.alt}
      />
    </>
  );
}