Ogni componente React è caratterizzato da un ciclo di vita. All’interno dei Class Component abbiamo accesso a dei metodi particolari (lifecycle hooks) che potremo usare per intercettare alcuni istanti del ciclo di vita di un componente e stabilire un determinato comportamento per quel componente. Per esempio potremmo eseguire determinate azioni quando un componente viene creato, aggiornato o distrutto o, addirittura, decidere se un componente deve essere modificato in seguito alla variazione di almeno una delle proprietà contenute nel suo oggetto State o alla ricezione di nuove Props.
Nel ciclo di vita di un componente React possiamo distinguere essenzialmente tre fasi:
- Inizializzazione del componente
- Aggiornamento del componente tramite la funzione setState() o la ricezione di nuove Props
- Rimozione del componente
Inizializzazione di un componente
Il costruttore è il primo metodo che viene chiamato. Per una corretta inizializzazione, all’interno del costruttore di un componente, dovremo innanzitutto invocare il costruttore della classe base super(props) (come abbiamo visto definiamo un componente estendendo React.Component) passando le props ricevute dal componente. Sempre in questa fase, verranno assegnati i valori di default alle props che abbiamo definito tramite l’oggetto defaultProps. (es. App.defaultProps = {prop1: ‘valore1’}) Verrà quindi inizializzato l’oggetto state.
Il secondo metodo ad essere invocato, immediatamente prima della funzione render(), è componentWillMount(). React consiglia di usare il costruttore al posto di questo metodo per ogni inizializzazione. Non si può accedere agli elementi del DOM. Se invochiamo la funzione setState() all’interno di questo metodo, l’oggetto state verrà aggiornato con il nuovo valore ma non verranno chiamati i metodi che solitamente vengono invocati inseguito all’esecuzione della funzione setState() (Non verranno invocati ShouldComponentUpdate(), ComponentWillUpdate e ComponentDidUpdate() ma solo il metodo render()).
La funzione render() è l’unico metodo obbligatorio quando si vuole definire un componente. Deve restituire un React Element oppure null o false, se non vogliamo mostrare nulla sullo schermo per quel componente. Deve essere una funzione pura. Non deve modificare direttamente l’oggetto props o state. All’interno della funzione render() non si deve manipolare il DOM o effettuare chiamate a funzioni per acquisire dati da un server. Vedremo che il concetto di funzione pura ritorna spesso in React e ritornerà quando parleremo di Redux. Una funzione pura, dati certi argomenti, deve restituire sempre lo stesso valore. Non fa uso dei valori definiti in variabili globali, non effettua operazioni di I/O o chiamate remote.
ComponentDidMount() viene invocata dopo la funzione render(). Questo è il metodo da usare per manipolare il DOM o per recuperare eventuali dati da un server. Le valutazioni fatte in merito all’oggetto State nel metodo componentWillMount() valgono anche per ComponentDidMount(). Vediamo un esempio.
class App extends React.Component {
constructor(props) {
console.log("constructor");
super(props);
this.state = {counter: 0};
}
componentWillMount() {
console.log("ComponentWillMount");
this.setState({counter: 1});
}
componentDidMount() {
console.log("ComponentDidMount");
}
shouldComponentUpdate(nextProps, nextState) {
console.log("ShouldComponentUpdate");
return true;
}
componentWillUpdate(nextProps, nextState) {
console.log("componentWillUpdate");
}
componentDidUpdate(nextProps, nextState) {
console.log("componentDidUpdate");
}
render() {
console.log("Render");
return(
<h1>{this.state.counter}</h1>
);
}
}
In fase di creazione del componente, vedremo come output nella console i seguenti messaggi.
E il componente così costruito sarà il seguente.
Aggiornamento del componente
Aggiornamento dell’oggetto State
Quando effettuiamo l’aggiornamento dell’oggetto State, vengono invocati una serie di metodi in sequenza.
Il primo è shouldComponentUpdate() che riceve come argomenti gli oggetti nextProps e nextState i quali rappresentano il prossimo valore dell’oggetto Props e dell’oggetto State. Il metodo shouldComponentUpdate() restituisce un valore booleano. Se restituisce false, i metodi successivi non verranno invocati. Di default questo metodo restituisce true, consentendo l’aggiornamento del componente ad ogni modifica degli oggetti State e Props. Per questo motivo, può essere usato per determinare se il componente deve essere aggiornato con i prossimi valori di Props e State. All’interno di questo metodo è infatti possibile comparare i valori attuali degli oggetti State e Props con quelli di nextState e nextProps, permettendo così di decidere se effettuare l’aggiornamento del componente, restituendo true o false. React offre la possibilità di creare un componente che estende React.PureComponent invece di React.Component. In React.PureComponent il metodo shouldComponentUpdate() è implementato in maniera tale da effettuare un confronto degli oggetti Props e State con una tecnica che in inglese viene definita Shallow comparison. Tradotto letteralmente vuol dire un confronto superficiale, poco profondo. In sostanza, viene effettuato il confronto dei due oggetti fino a un primo livello di profondità. Se le proprietà all’interno dei due oggetti hanno lo stesso nome, sono dei tipi primitivi e contengono un valore uguale, allora i due oggetti saranno uguali.(They are shallowly equal) Se può risultare complicato capire cosa vuol dire, vediamo subito un esempio che chiarisca il concetto. Useremo il package npm shallow-equal e per semplicità useremo Node.js. (Se non l’avete ancora fatto vi consiglio di installare node.js e npm sui vostri computer e aprire un account gratuito su npmjs.com anche se non è indispensabile per usare npm. Parleremo in maggiore dettaglio di npm e yarn in uno dei prossimi articoli).
const shallowEqualObjects = require('shallow-equal/objects');
const obj1 = {
prop1: "Ciao",
prop2: 4,
prop3: null,
prop4: true
};
const obj2 = {
prop1: "Ciao",
prop2: 4,
prop3: null,
prop4: true
};
const obj3 = {
prop1: "Ciao",
prop2: 4,
prop3: null,
prop4: false // prop4 diversa da obj1.prop4
};
obj4 = obj1;
/* prop3 è uguale in obj5 e obj6 ma
* è un array per cui shallowEqualObjects(obj5, obj6)
* restituisce false
*/
const obj5 = {
prop1: "Ciao",
prop2: 4,
prop3: [1, 2, 3],
prop4: false
};
/* prop3 è uguale in obj5 e obj6 ma
* è un array per cui shallowEqualObjects(obj5, obj6)
* restituisce false
*/
const obj6 = {
prop1: "Ciao",
prop2: 4,
prop3: [1, 2, 3],
prop4: false
};
const obj7 = { a: 5, b: {} };
const obj8 = { a: 5, b: {} }
console.log("Compare obj1 and obj2 with ==== ", obj1 === obj2); // false
console.log("Compare obj1 and obj4 with ==== ", obj1 === obj4); // true
console.log("obj1 dhallow equal obj2? ", shallowEqualObjects(obj1, obj2)); // true
console.log("obj1 dhallow equal obj3? ", shallowEqualObjects(obj1, obj3)); // false
console.log("obj1 dhallow equal obj4? ", shallowEqualObjects(obj1, obj4)); // true
console.log("obj5 dhallow equal obj6? ", shallowEqualObjects(obj5, obj6)); // false
console.log("obj7 dhallow equal obj8? ", shallowEqualObjects(obj7, obj8)) // false
Se shouldComponentUpdate() restituisce true, verrà allora invocata componentWillUpdate(). In questo metodo, non è consentito modificare l’oggetto State tramite this.setState(). Il metodo componentWillUpdate() può essere utilizzato per preparare il componente prima che avvenga l’aggiornamento dell’oggetto State o dell’oggetto Props, visto che questo metodo viene chiamato anche in seguito alla ricezione di nuove proprietà.
Dopo il metodo render(), viene invocato componentDidUpdate() che può essere usato per scaricare dati aggiornati da un server (nel caso per esempio ci sia stato un aggiornamento dell’oggetto Props o State) o per operare sul DOM.
Vediamo un semplice esempio. Ci limiteremo a stampare nella console un messaggio per ogni metodo invocato in seguito all’aggiornamento dell’oggetto State.
class App extends React.Component {
constructor(props) {
console.log("constructor");
super(props);
this.state = {counter: 0};
}
increment() {
this.setState((prevState, props) => ({
counter: prevState.counter + 1
})
);
}
componentWillMount() {
console.log("ComponentWillMount");
this.setState({counter: 1});
}
componentDidMount() {
console.log("ComponentDidMount");
}
shouldComponentUpdate(nextProps, nextState) {
console.log("ShouldComponentUpdate");
return true;
}
componentWillUpdate(nextProps, nextState) {
console.log("componentWillUpdate");
}
componentDidUpdate(nextProps, nextState) {
console.log("componentDidUpdate");
}
render() {
console.log("Render");
return(
<div>
<h1>{this.state.counter}</h1>
// ad ogni click, aggiorniamo l'oggetto State
<button onClick={() => this.increment()}>Incrementa</button>
</div>
);
}
}
Ad ogni click del pulsante, incrementiamo this.state.counter. Nella console stampiamo dei messaggi per ogni metodo invocato in seguito alla chiamata della funzione this.setState().
Ricezione di nuove Props
Anche nel caso in cui un componente riceva nuove Props, una serie di metodi verranno invocati. La maggior parte di questi sono gli stessi metodi appena visti nel caso dell’oggetto State.
In questo caso avremo però un metodo aggiuntivo: ComponentWillReceiveProps(). React non invoca questo metodo in fase di inizializzazione di un componente. Viene invece invocato solo successivamente, nel momento in cui nuove Props vengono passate a un componente. Può essere utilizzato per aggiornare e modificare l’oggetto State all’interno di un componente se le nuove Props ricevute sono diverse dalle proprietà dell’oggetto Props corrente.
class Child extends React.Component {
constructor(props) {
console.log("Child - constructor");
super(props);
}
componentWillMount() {
console.log("Child - ComponentWillMount");
}
componentDidMount() {
console.log("Child - ComponentDidMount");
}
componentWillReceiveProps(nextProps) {
console.log("Child - componentWillReceiveProps");
}
shouldComponentUpdate(nextProps, nextState) {
console.log("Child - ShouldComponentUpdate");
return true;
}
componentWillUpdate(nextProps, nextState) {
console.log("Child - componentWillUpdate");
}
componentDidUpdate(nextProps, nextState) {
console.log("Child - componentDidUpdate");
}
render() {
console.log("Child - Render");
return(
<h1>{this.props.counter}</h1>
);
}
}
class App extends React.Component {
constructor(props) {
console.log("App - constructor");
super(props);
this.state = {counter: 0};
}
increment() {
this.setState((prevState, props) => ({
counter: prevState.counter + 1
})
);
}
componentWillMount() {
console.log("App - ComponentWillMount");
this.setState({counter: 1});
}
componentDidMount() {
console.log("App - ComponentDidMount");
}
shouldComponentUpdate(nextProps, nextState) {
console.log("App - ShouldComponentUpdate");
return true;
}
componentWillUpdate(nextProps, nextState) {
console.log("App - componentWillUpdate");
}
componentDidUpdate(nextProps, nextState) {
console.log("App - componentDidUpdate");
}
render() {
console.log("App - Render");
return(
<div>
<Child counter={this.state.counter} />
<button onClick={() => this.increment()}>Incrementa</button>
</div>
);
}
}
L’immagine sottostante mostra quello che viene stampato nella console in fase di inizializzazione.
Nel momento in cui effettueremo un click sul pulsante Incrementa, verrà aggiornato l’oggetto State del componente App e il nuovo valore aggiornato sarà passato come prop al componente Child. Per cui nella console saranno stampati i seguenti messaggi.
Distruzione di un componente
Vediamo infine cosa succede in fase di smontaggio di un componente. Verrà invocato come unico metodo componentWillUnmount() che potremo usare per operazioni come invalidare eventuali timer avviati nella funzione componentDidMount() e altre operazioni di manutenzione che prevengano memory leak. Riprendiamo l’esempio visto in precedenza e modifichiamolo introducendo componentWillUnmount() nel componente Child.
const rootNode = document.querySelector('.root');
class Child extends React.Component {
constructor(props) {
console.log("Child - constructor");
super(props);
}
componentWillMount() {
console.log("Child - ComponentWillMount");
}
componentDidMount() {
console.log("Child - ComponentDidMount");
}
componentWillReceiveProps(nextProps) {
console.log("Child - componentWillReceiveProps");
}
shouldComponentUpdate(nextProps, nextState) {
console.log("Child - ShouldComponentUpdate");
return true;
}
componentWillUpdate(nextProps, nextState) {
console.log("Child - componentWillUpdate");
}
componentDidUpdate(nextProps, nextState) {
console.log("Child - componentDidUpdate");
}
componentWillUnmount() {
console.log("Child - componentWillUnmount");
}
render() {
console.log("Child - Render");
return(
<h1>{this.props.counter}</h1>
);
}
}
class App extends React.Component {
constructor(props) {
console.log("App - constructor");
super(props);
// cambiamo this.state
this.state = {counter: 0, child: true};
}
increment() {
this.setState((prevState, props) => ({
counter: prevState.counter + 1
})
);
}
// aggiungiamo un metodo unmount()
unmount() {
this.setState({child: false});
}
// aggiungiamo un metodo mount()
mount() {
this.setState({child: true});
}
componentWillMount() {
console.log("App - ComponentWillMount");
this.setState({counter: 1});
}
componentDidMount() {
console.log("App - ComponentDidMount");
}
shouldComponentUpdate(nextProps, nextState) {
console.log("App - ShouldComponentUpdate");
return true;
}
componentWillUpdate(nextProps, nextState) {
console.log("App - componentWillUpdate");
}
componentDidUpdate(nextProps, nextState) {
console.log("App - componentDidUpdate");
}
render() {
console.log("App - Render");
let child = null;
/* se this.state.child è true, assegniamo alla variabile child
* il React Element <Child />
*/
child = this.state.child ? <Child counter={this.state.counter} /> : null;
return(
<div>
{child}
<button onClick={() => this.increment()}>Increment</button>
<button onClick={() => this.unmount()}>Unmount Child Component</button>
<button onClick={() => this.mount()}>Mount Child Component</button>
</div>
);
}
}
ReactDOM.render(<App />, rootNode);
Nel metodo render() abbiamo ora tre pulsanti. Se facciamo click su ‘Unmount Child Component’, verrà chiamata il metodo this.unmount() che aggiornerà l’oggetto State. Nel metodo render() verifichiamo che this.state.child sia uguale a true. In questo caso la variabile child conterrà il React Element <Child /> che sarà quindi mostrato sullo schermo. Premendo sul pulsante ‘Unmount Child Component’, <Child /> sarà rimosso. Per cui nella console verranno stampati i seguenti messaggi.
Nel prossimo articolo vediamo come interagire con i componenti. Spiegheremo in dettaglio come registrare delle funzioni che verranno chiamate al verificarsi di certi eventi. (per esempio quando si fa click su un pulsante) Ci soffermeremo, inoltre, sull’uso della keyword this all’interno dei componenti, dato il comportamento un po’ anomalo in Javascript rispetto ad altri linguaggi di programmazione.