State e Lifecycle

Questa pagina introduce il concetto di state (stato) e lifecycle (ciclo di vita) in un componente React. Puoi trovare un riferimento dettagliato alle API dei componenti qui.

Considera l’esempio dell’orologio di una delle sezioni precedenti. In Renderizzare Elementi, abbiamo appreso solamente un modo per aggiornare la UI. Chiamiamo ReactDOM.render() per cambiare l’output renderizzato:

function tick() {
  const element = (
    <div>
      <h1>Ciao, mondo!</h1>
      <h2>Sono le {new Date().toLocaleTimeString()}.</h2>
    </div>
  );
  ReactDOM.render(
    element,
    document.getElementById('root')
  );
}

setInterval(tick, 1000);

Prova su CodePen

In questa sezione, apprenderemo come rendere il componente Clock davvero riutilizzabile ed incapsulato. Esso si occuperà di impostare il proprio timer e di aggiornarsi ogni secondo.

Possiamo iniziare incapsulando l’aspetto dell’orologio:

function Clock(props) {
  return (
    <div>
      <h1>Ciao, mondo!</h1>
      <h2>Sono le {props.date.toLocaleTimeString()}.</h2>
    </div>
  );
}

function tick() {
  ReactDOM.render(
    <Clock date={new Date()} />,
    document.getElementById('root')
  );
}

setInterval(tick, 1000);

Prova su CodePen

Tuttavia, manca un requisito fondamentale: il fatto che Clock imposti un timer ed aggiorni la propria UI ogni secondo dovrebbe essere un dettaglio implementativo di Clock.

Idealmente, vorremmo scrivere il seguente codice una volta sola, ed ottenere che Clock si aggiorni da solo:

ReactDOM.render(
  <Clock />,
  document.getElementById('root')
);

Per implementare ciò, abbiamo bisogno di aggiungere uno “stato” al componente Clock.

Lo state (o stato) è simile alle props, ma è privato e completamente controllato dal componente.

Convertire una Funzione in una Classe

Puoi convertire un componente funzione come Clock in una classe in cinque passaggi:

  1. Crea una classe ES6, con lo stesso nome, che estende React.Component.

  2. Aggiungi un singolo metodo vuoto chiamato render().

  3. Sposta il corpo della funzione all’interno del metodo render().

  4. Sostituisci props con this.props nel corpo del metodo render().

  5. Rimuovi la dichiarazione della funzione rimasta vuota.

class Clock extends React.Component {
  render() {
    return (
      <div>
        <h1>Ciao, mondo!</h1>
        <h2>Sono le {this.props.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

Prova su CodePen

Clock è ora definito da una classe, invece che da una funzione.

Il metodo render viene invocato ogni volta che si verifica un aggiornamento, ma finché renderizziamo <Clock /> nello stesso nodo del DOM, verrà utilizzata un’unica istanza della classe Clock. Questo ci consente di utilizzare funzionalità aggiuntive come il local state e i metodi del lifecycle del componente.

Aggiungere il Local State ad una Classe

Sposteremo date dalle props allo state in tre passaggi:

  1. Sostituisci this.props.date con this.state.date nel metodo render():
class Clock extends React.Component {
  render() {
    return (
      <div>
        <h1>Ciao, mondo!</h1>
        <h2>Sono le {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}
  1. Aggiungi un costruttore di classe che assegna il valore iniziale di this.state:
class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }

  render() {
    return (
      <div>
        <h1>Ciao, mondo!</h1>
        <h2>Sono le {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

Nota come passiamo props al costruttore di base:

  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }

I componenti classe dovrebbero sempre chiamare il costruttore base passando props come argomento.

  1. Rimuovi la prop date dall’elemento <Clock />:
ReactDOM.render(
  <Clock />,
  document.getElementById('root')
);

In seguito ci occuperemo di aggiungere la parte di codice relativa al timer all’interno del componente stesso.

Il risultato dovrebbe avere questo aspetto:

class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }

  render() {
    return (
      <div>
        <h1>Ciao, mondo!</h1>
        <h2>Sono le {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

ReactDOM.render(
  <Clock />,
  document.getElementById('root')
);

Prova su CodePen

Adesso, faremo in modo che Clock imposti il proprio timer e si aggiorni ogni secondo.

Aggiungere Metodi di Lifecycle ad una Classe

Nelle applicazioni con molti componenti, è molto importante rilasciare le risorse occupate dai componenti quando questi vengono distrutti.

Nel nostro caso, vogliamo impostare un timer ogni volta che Clock è renderizzato nel DOM per la prima volta. Questo è definito “mounting” (“montaggio”) in React.

Vogliamo anche cancellare il timer ogni volta che il DOM prodotto da Clock viene rimosso. Questo è definito “unmounting” (“smontaggio”) in React.

Possiamo dichiarare alcuni metodi speciali nel componente classe per eseguire del codice quando il componente viene montato e smontato:

class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }

  componentDidMount() {

  }

  componentWillUnmount() {

  }

  render() {
    return (
      <div>
        <h1>Ciao, mondo!</h1>
        <h2>Sono le {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

Questi metodi sono chiamati “metodi del lifecycle” (metodi del ciclo di vita).

Il metodo componentDidMount() viene eseguito dopo che l’output del componente è stato renderizzato nel DOM. È un buon punto in cui impostare un timer:

  componentDidMount() {
    this.timerID = setInterval(
      () => this.tick(),
      1000
    );
  }

Nota come salviamo l’ID del timer direttamente in this.

Mentre this.props viene impostato da React stesso e this.state ha un significato speciale, sei libero di aggiungere altri campi alla classe se hai bisogno di salvare qualcosa che non partecipa al flusso dei dati (come l’ID di un timer).

Ci occuperemo di cancellare il timer nel metodo del lifecycle componentWillUnmount():

  componentWillUnmount() {
    clearInterval(this.timerID);
  }

Infine, implementeremo un metodo chiamato tick() che verrà invocato dal componente Clock ogni secondo.

Il nuovo metodo utilizzerà this.setState() per pianificare gli aggiornamenti al local state del componente:

class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }

  componentDidMount() {
    this.timerID = setInterval(
      () => this.tick(),
      1000
    );
  }

  componentWillUnmount() {
    clearInterval(this.timerID);
  }

  tick() {
    this.setState({
      date: new Date()
    });
  }

  render() {
    return (
      <div>
        <h1>Ciao, mondo!</h1>
        <h2>Sono le {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

ReactDOM.render(
  <Clock />,
  document.getElementById('root')
);

Prova su CodePen

In questo modo l’orologio scatta ogni secondo.

Ricapitoliamo velocemente quello che sta succedendo e l’ordine con cui i metodi sono invocati:

  1. Quando <Clock /> viene passato a ReactDOM.render(), React invoca il costruttore del componente Clock. Dal momento che Clock ha bisogno di mostrare l’ora corrente, inizializza this.state con un oggetto che include l’ora corrente. In seguito, aggiorneremo questo state.

  2. In seguito, React invoca il metodo render() del componente Clock. Questo è il modo in cui React apprende cosa dovrebbe essere visualizzato sullo schermo. React si occupa di aggiornare il DOM in modo da farlo corrispondere all’output della renderizzazione di Clock.

  3. Quando l’output della renderizzazione di Clock viene inserito nel DOM, React invoca il metodo del lifecycle componentDidMount(). Al suo interno, il componente Clock chiede al browser di impostare un timer con cui invocare il metodo tick() del componente una volta al secondo.

  4. Ogni secondo, il browser invoca il metodo tick(). Al suo interno, il componente Clock pianifica un aggiornamento della UI invocando setState() con un oggetto che contiene la nuova ora corrente. Grazie alla chiamata a setState(), React viene informato del fatto che lo state è cambiato e invoca di nuovo il metodo render() per sapere che cosa deve essere mostrato sullo schermo. Questa volta, this.state.date nel metodo render() avrà un valore differente, di conseguenza l’output della renderizzazione includerà l’orario aggiornato. React aggiorna il DOM di conseguenza.

  5. Se il componente Clock dovesse mai essere rimosso dal DOM, React invocherebbe il metodo del lifecycle componentWillUnmount() ed il timer verrebbe cancellato.

Utilizzare Correttamente lo Stato

Ci sono tre cose che devi sapere a proposito di setState().

Non Modificare lo Stato Direttamente

Per esempio, questo codice non farebbe ri-renderizzare un componente:

// Wrong
this.state.comment = 'Hello';

Devi invece utilizzare setState():

// Correct
this.setState({comment: 'Hello'});

L’unico punto in cui puoi assegnare direttamente un valore a this.state è nel costruttore.

Gli Aggiornamenti di Stato Potrebbero Essere Asincroni

React potrebbe accorpare più chiamate a setState() in un unico aggiornamento per migliorare la performance.

Poiché this.props e this.state potrebbero essere aggiornate in modo asincrono, non dovresti basarti sul loro valore per calcolare lo stato successivo.

Ad esempio, questo codice potrebbe non riuscire ad aggiornare correttamente il contatore:

// Sbagliato
this.setState({
  counter: this.state.counter + this.props.increment,
});

Per effettuare correttamente questa operazione, bisogna utilizzare una seconda forma di setState() che accetta in input una funzione invece che un oggetto. Quella funzione riceverà come primo argomento lo stato precedente e come secondo argomento le proprietà, aggiornate al momento in cui l’aggiornamento di stato è applicato:

// Giusto
this.setState((state, props) => ({
  counter: state.counter + props.increment
}));

Qui abbiamo utilizzato una arrow function, ma puoi utilizzare anche una funzione tradizionale:

// Giusto
this.setState(function(state, props) {
  return {
    counter: state.counter + props.increment
  };
});

Gli Aggiornamenti di Stato Vengono Applicati Tramite Merge

Quando chiami setState(), React effettua il merge dell’oggetto che fornisci nello state corrente.

Ad esempio, il tuo state potrebbe contenere molte variabili indipendenti:

  constructor(props) {
    super(props);
    this.state = {
      posts: [],
      comments: []
    };
  }

A questo punto puoi aggiornarle indipendentemente con invocazioni separate del metodo setState():

  componentDidMount() {
    fetchPosts().then(response => {
      this.setState({
        posts: response.posts
      });
    });

    fetchComments().then(response => {
      this.setState({
        comments: response.comments
      });
    });
  }

Quello che viene effettuato è uno “shallow merge”, quindi this.setState({comments}) lascia intatto this.state.posts, ma sostituisce completamente this.state.comments.

I Dati Fluiscono Verso il Basso

Né i componenti genitori né i componenti figli possono sapere se un certo componente è “stateful” o “stateless” (cioè se è dotato o meno di stato) e non dovrebbero preoccuparsi del fatto di essere definiti come funzione o come classe.

Questa è la ragione per cui lo stato è spesso definito locale o incapsulato. Esso non è accessibile a nessun componente a parte quello a cui appartiene.

Un componente potrebbe decidere di passare il suo stato ai componenti figli sotto forma di props:

<h2>Sono le {this.state.date.toLocaleTimeString()}.</h2>

Questo funziona anche con i componenti definiti dall’utente:

<FormattedDate date={this.state.date} />

Il componente FormattedDate riceve date nelle sue props e non può sapere se viene dallo state di Clock, dalle proprietà di Clock o se è stato inserito a mano:

function FormattedDate(props) {
  return <h2>Sono le {props.date.toLocaleTimeString()}.</h2>;
}

Prova su CodePen

Questo è spesso definito flusso di dati “top-down” (dall’alto verso il basso) o “unidirezionale”. In questo paradigma, lo stato è sempre posseduto da uno specifico componente, e tutti i dati o la UI derivati da quello stato possono influenzare solamente i componenti “più in basso” nell’albero.

Se immagini un albero di componenti come una cascata di props, puoi pensare allo stato di ciascun componente come a una sorgente d’acqua aggiuntiva che si unisce alla cascata in un punto qualsiasi e flusice verso il basso insieme al resto dell’acqua.

Per mostrare che tutti i componenti sono davvero isolati, possiamo creare un componente App che renderizza tre <Clock>:

function App() {
  return (
    <div>
      <Clock />
      <Clock />
      <Clock />
    </div>
  );
}

ReactDOM.render(
  <App />,
  document.getElementById('root')
);

Prova su CodePen

Ciascun Clock imposta il proprio timer e si aggiorna indipendentemente dagli altri.

Nelle applicazioni React, il fatto che un componente sia stateful o stateless è considerato un dettaglio implementativo di quel componente, che potrebbe cambiare nel tempo. Puoi utilizzare componenti stateless all’interno di componenti stateful, e viceversa.