back to top

Gestione degli eventi nei componenti React

Per realizzare un’applicazione dinamica, è indispensabile permettere agli utenti di interagire con i vari componenti che la compongono.

Grazie all’uso di JSX, possiamo registrare un gestore di eventi (Event Handler) direttamente sugli elementi in maniera simile a come faremmo in un documento HTML.

Abbiamo già visto alcuni esempi negli articoli precedenti in cui passavamo la proprietà onClick ad un pulsante. Nel momento in cui si cliccava su quel pulsante, veniva invocato un metodo definito all’interno del componente.

React usa la notazione camelCase per i nomi degli eventi (onClick, onKeyDown, onKeyPress ecc…) e in JSX registriamo una funzione come gestore di un dato evento passando come attributo un riferimento a quella funzione.

Vediamo subito un esempio.

// Esempio 1

class App extends React.Component {
  constructor(props) {
    super(props);
  }
  handleClick() {
    console.log("Pulsante premuto - Evento click");
  }
  render() {
    return (
      /* passiamo un riferimento al metodo handleClick */
      <button onClick={this.handleClick} >Pulsante</button>
    )
  }
}

Facendo click sul pulsante, verrà stampato nella console un messaggio. Abbiamo infatti passato all’elemento <button></button> la prop onClick che riceve fra parentesi graffe il riferimento al metodo this.handleClick. (da notare che abbiamo passato un riferimento alla funzione e non l’abbiamo invocata). In questo modo, ogni volta che il pulsante riceverà il click del mouse verrà invocato il metodo handleClick().

In realtà, manca qualcosa nell’esempio appena visto. Infatti, React passerà implicitamente alla funzione handleClick() un argomento che è un’istanza di SyntheticEvent, un wrapper cross-browser dell’ oggetto Evento nativo del browser. Il SyntheticEvent nasconde le peculiarità, funzionalità e differenze di implementazione degli oggetti Event nei diversi browser e garantisce un funzionamento uniforme. Non dovremo preoccuparci di casi particolari che potrebbero accadere in un determinato browser e avremo a disposizione gli stessi metodi e proprietà che avremmo avuto se avessimo usato il semplice linguaggio Javascript. React ci permette comunque di avere accesso all’oggetto Event originale tramite la proprietà nativeEvent disponibile su ogni istanza di SyntheticEvent.

Correggiamo l’esempio visto sopra passando a handleClick() un primo argomento che per convenzione chiameremo semplicemente ‘e’.

// Esempio 2 
class App extends React.Component {
  constructor(props) {
    super(props);
  }
  handleClick(e) {
    console.log(`Pulsante premuto - Evento ${e.type}`);
  }
  render() {
    return (
      /* passiamo un riferimento al metodo handleClick */
      <button onClick={this.handleClick} >Pulsante</button>
    )
  }
}

Qualora fosse necessario è sempre possibile invocare e.preventDefault() che cancella l’azione predefinita associata a un evento oppure e.stopPropagation() che impedisce la propagazione dell’evento corrente. Inoltre l’oggetto ‘e‘ presenta, come già detto, la stessa interfaccia dell’oggetto Event nativo del browser, per cui è possibile accedere agli stessi metodi e proprietà a cui siamo abituati.

Negli articoli precedenti, se avete notato, nell’attributo onClick è stata usata un’arrow function invece di un riferimento a qualche funzione. All’interno dell’arrow function è stato invocato un metodo definito nel nostro componente. La scelta non è stata casuale e, prima di vedere un metodo più efficiente da poter usare al posto delle arrow function, cerchiamo di capire il perché di quella scelta.

Nell’esempio appena visto, la funzione handleClick() si limita a stampare delle informazioni nella console. Negli esempi visti negli articoli precedenti, invece, all’interno della funzione che veniva invocata nell’arrow function, passata all’attributo onClick, eseguivamo this.setState(). Riportiamo un esempio semplificato che possa aiutare a visualizzare meglio il tutto.

// Esempio 3
class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {event: '-'};
  }
  handleClick(e) {
    this.setState({event: e.type});
  }
  render() {
    return (
      <div>
        <p>Tipo di evento: {this.state.event}</p>
        <button onClick={(e) => this.handleClick(e)} >Pulsante</button>  
      </div>
    )
  }
}

In questo caso alla nostra funzione passiamo anche l’oggetto ‘e’. Al click del pulsante, this.state.event verrà settato esattamente al valore "click". Il motivo che ci ha spinto a scegliere di impiegare le arrow function, è la necessità di usare la keyword this quando invochiamo la funzione this.setState().

Vediamo cosa accadrebbe se passassimo all’attributo onClick un riferimento alla funzione handleClick.

// Esempio 4

// Versione errata
class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {event: '-'};
  }
  handleClick(e) {
    this.setState({event: e.type});
  }
  render() {
    return (
      <div>
        <p>Tipo di evento: {this.state.event}</p>
        {/* Errore! Non fatelo a casa :) */}
        <button onClick={this.handleClick} >Pulsante</button>  
      </div>
    )
  }
}

Nella console verrà stampato un messaggio di errore "Cannot read property ‘setState’ of null".

messaggio di errore Cannot read property 'setState' of null

La documentazione ufficiale di React ci avverte di far attenzione a questo tipo di errori.

Questo tipo di comportamento non è specifico di React, ma è tipico del linguaggio Javascript. In Javascript, nel caso delle "normali" funzioni, l’oggetto a cui this fa riferimento viene deciso a run-time. In maniera un po’ approssimativa e brutale possiamo dire che quando invochiamo una funzione (istanza.metodo()), il this all’interno della funzione (metodo()) fa riferimento all’oggetto a sinistra del punto (istanza). In strict mode, se non è specificato alcun oggetto e viene invocata semplicemente la funzione, il valore di this sarà semplicemente uguale a "undefined".

La keyword ‘this’ in Javascript

Per risalire allora alla causa dell’errore visto sopra dobbiamo abbandonare per un momento React e fare qualche esempio in Javascript.

// Esempio 5

"use strict";

function foo(){
  return this;
}

console.log(foo() === undefined); // true


class Baz {
    constructor() {
      console.log("Istanza di Baz creata");
      this.prop = "Baz prop";
    }
    bazFunc() {
      console.log(this.prop);
    }
    receiveCallback(callback) {
      console.log("receiveCallback called");
      // invochiamo la callback ricevuta come parametro
      callback();
    }
}

class Bat {
    constructor() {
      console.log("Istanza di Bat creata");
      this.prop = "Bat prop";
    }
    batFunc() {
      console.log(this.prop);
    }
}

const baz = new Baz();
const bat = new Bat();

baz.bazFunc() // "Baz prop"
bat.batFunc() // "Bat prop"

// 'this' sarà undefined
baz.receiveCallback(bat.batFunc); // Errore!

Negli esempi riportati sopra, possiamo notare che quando invochiamo la funzione foo(), this sarà pari a ‘undefined’. All’ultimo rigo del nostro esempio, quando chiamiamo baz.receiveCallback() e passiamo un riferimento al metodo bat.batFunc, riceveremo il seguente errore nella console degli strumenti per sviluppatori.

 Messaggio di errore Cannot read property 'prop' of undefined

Il motivo è che il valore di this all’interno di batFunc() non viene determinato nel momento in cui definiamo il metodo all’interno della "classe" Bat. Quando invochiamo callback(), eseguiamo il corpo della funzione batFunc(), ma nel nuovo contesto di esecuzione this è ‘undefined’.

Diverso è il discorso per le arrow function in cui il valore di this è assegnato lessicalmente ovvero il suo valore è determinato staticamente e corrisponde al valore del contesto di esecuzione che contiene this. In altre parole this sarà l’oggetto in cui l’arrow function viene definita. Nel caso dell’esempio visto sopra (Esempio 3), in cui abbiamo creato un componente App e assegnato un’arrow function all’attributo onClick, tutto funziona correttamente perché quando viene invocata this.setState(), this corrisponderà all’istanza del componente App.

Vediamo quindi qualche esempio in Javascript in cui useremo delle arrow function.

// Esempio 6

"use strict";

const globalObject = this;

const foo = () => this;

// globalObject === window nel browser
console.log(foo() === globalObject); // true


class Baz {
  constructor(bat) {
    this.prop = "Baz prop";
    this.bat = bat;
  }
  receiveCallback(callback) {
    const callbackResult = callback();
    console.log(
      "this.bat === callbackResult? ", 
      this.bat === callbackResult
    ); // true nel caso dell'arrow function
  }
}

class Bat {
  constructor() {
    this.prop = "Bat prop";
  }
  normalFunc() {
    return function() {
      console.log(this.prop);
    }
  }
  arrowFunc() {
    return () => {
      console.dir("L'arrow function restituita da arrowFunc() ha accesso a: " + this.prop);
      return this;
    }
  }
}

const bat = new Bat();
const baz = new Baz(bat);

// NOTA: stiamo invocando la funzione arrowFunc()
// 'this' punta all'oggetto bat
baz.receiveCallback(bat.arrowFunc()); // "Bat prop"


// NOTA: stiamo invocando la funzione normalFunc()
// 'this' sarà undefined
baz.receiveCallback(bat.normalFunc()); // Error! Cannot read property 'prop' of undefined

Come è possibile vedere dall’esempio, nel caso delle arrow function, il valore di this è uguale all’istanza dell’oggetto in cui sono state definite le funzioni.

Dalle arrow function a Function.prototype.bind

Tornando a parlare di React, potremmo quindi usare delle arrow function come event handler all’interno dei nostri componenti (Così come abbiamo fatto negli articoli precedenti e nell’esempio 3 visto sopra). Il problema è che ogni volta che viene invocato il metodo render() di un componente, viene creata una callback diversa. Passare quindi delle arrow function come prop può risultare inefficiente. Per risolvere tutte queste complicazioni, useremo la funzione Function.prototype.bind(). All’interno del costruttore dei nostri componenti scriveremo del codice come il seguente.

// Esempio 7
class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {event: '-'};
    // usiamo Function.prototype.bind() all'interno del costruttore
    this.handleClick = this.handleClick.bind(this);
  }
  handleClick(e) {
    this.setState({event: e.type});
  }
  render() {
    return (
      <div>
        <p>Tipo di evento: {this.state.event}</p>
        {/* Passiamo un riferimento alla funzione this.handleClick */}
        <button onClick={this.handleClick} >Pulsante</button>  
      </div>
    )
  }
}

La funzione Function.prototype.bind(nuovoThis[, arg1[, arg2[, …]]]) viene invocata su una funzione (nel nostro caso this.handleClick). Per spiegare come si comporta la funzione Function.prototype.bind, consideriamo il seguente esempio.

// Esempio 8

bFoo = foo.bind(newThis, arg1, arg2);

bFoo(arg3, arg4)

Nell’esempio appena visto, bFoo sarà una funzione avente lo stesso corpo della funzione foo (sarà una copia di foo), ma il valore di this, all’interno di bFoo, sarà uguale al valore di newThis (il primo argomento della funzione bind). I valori di arg3 e arg4 (passati a bFoo) verranno concatenati ad arg1 e arg2 e passati alla funzione bFoo nell’oggetto arguments disponibile in tutte le funzioni in Javascript.

A questo punto può risultare scomodo dover invocare nel costruttore di ciascun componente, per uno o più metodi, la funzione Function.prototype.bind(). Potremo risolvere, creando una funzione o un Decorator che svolga questo compito noioso al posto nostro. In questa guida, tuttavia, al fine di semplificare e rendere chiaro ed esplicito il codice dei nostri esempi, ci limiteremo a usare la sintassi già vista nell’esempio 7 e invocheremo manualmente la funzione Function.prototype.bind() quando ne avremo bisogno.

Pubblicitร