Patterns di binding del this in React, i pitfalls più comuni

P

In questo articolo presumerò che utilizziate e conosciate la libreria di Facebook React.js.
Da un po’ ormai sto lavorando su alcuni progetti in React (React Native per l’esattezza, ma poco importa nel contesto che andremo a esplorare) e lungo tutto questo periodo mi è capitato di riconoscere nel codice dei miei colleghi, o in rete, almeno 4 pattern comuni per ‘connettere’* i metodi di classe alla classe stessa di un componente.

*per connettere s’intende dare al this il valore dell’istanza del componente su cui stiamo lavorando.

Di cosa sto parlando?

Vediamo un esempio al volo (preso dalla doc ufficiale):

class SayHello extends React.Component {
  constructor(props) {
    super(props);
    this.state = {message: 'Hello!'};

    // Binding necessario per accede all'oggetto state all'interno del metodo handleClick.
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    alert(this.state.message);
  }

  render() {
    return (
      <button onClick={this.handleClick}>
        Say hello
      </button>
    );
  }
}

In questo estratto stiamo analizzando un componente definito attraverso la sintassi standardizzata da ES6 per la creazione di classi e, come possiamo facilmente vedere, il bottone una volta cliccato chiama il metodo di classe handleClick che grazie al bind effettuato nel costruttore, può referenziare lo stato attraverso il this.
Come già scritto nel codice sotto forma di commento, il bind è necessario per ‘far puntare’ il this alla classe in modo da poter referenziare, successivamente, la funzione handleClick.

Nulla di troppo strano, tutto ciò non è ovviamente dovuto a React piuttosto al modo in cui le funzioni sono state disegnate in JavaScript. Più qui

Ci sono però diversi modi sia per creare un componente, sia per gestire la questione del this.

2 diversi modi di creare un componente

Ebbene ci sono più modi per creare un componente stateful a partire da una classe (non sto prendendo in considerazione i functional/stateless component chiaramente) di cui uno di questi è decisamente sconsigliato ma lo propongo comunque come esempio per introdurre il concetto di autobinding.

Incluso in React, nemmeno troppo tempo fa, avevamo a disposizione un metodo chiamato “createClass” da utilizzare per la creazione di un componente che eseguiva automaticamente il bind dei metodi di quella classe in modo da poterli referenziare ovunque al suo interno. Per scoraggiare l’utilizzo di questa metodologia, preferendo invece la maniera standard, questo metodo è stato tirato fuori dalla libreria principale e portato in un modulo (qui le doc).

L’esempio sconsigliato che segue fa uso di questo modulo: proviamo quindi a riscrivere il componente del paragrafo precedente:

var SayHello = createReactClass({
  getInitialState: function() {
    return {message: 'Hello!'};
  },

  handleClick: function() {
    alert(this.state.message);
  },

  render: function() {
    return (
      <button onClick={this.handleClick}>
        Say hello
      </button>
    );
  }
});

Come vedete il codice è molto meno verboso, vediamo ora come utilizzare le classi standard ES6 e rendere il codice breve ma, soprattutto, efficiente.

I 4 pattern comuni

Vedremo prima due antipattern, ovvero due metodi molto spesso utilizzati ma che possono portare a seri e gravi problemi di performance. Poi continueremo con gli altri due di cui l’ultimo è un perfezionamento del penultimo.

1. Binding in render – sbagliato

In questo pattern, il binding viene effettuato a tempo di render. Ovvero ogniqualvolta React chiama la funzione ‘render’ per mostrare il nostro componente, creiamo una nuova funzione passandogli il this corretto. Riprendendo sempre il primo esempio questo è il codice:

class SayHello extends React.Component {
  constructor(props) {
    super(props);
    this.state = {message: 'Hello!'};
  }

  handleClick() {
    alert(this.state.message);
  }

  render() {
    return (
      // Qui il codice interessato
      <button onClick={this.handleClick.bind(this)}>
        Say hello
      </button>
    );
  }
}

Qual è il problema in questo approccio? Proprio il fatto che ricreiamo una nuova funzione ad ogni render. Oltre lo spreco computazionale inutile, quanto irrisorio, di creare un nuovo riferimento in memoria e lasciare che il Garbage Collector si prenda quello vecchio senza motivo, sintassi del genere uccidono qualunque tipo di ottimizzazione effettuata all’interno del componentShouldUpdate.
Per chi usa Redux, sa di cosa parlo e affiancare alla funzione connect un componente così definito è il peggio che potete fare. Qui un po’ di spiegazioni aggiuntive sul connect di Redux, qui un po’ di informazioni sul componentShouldUpdate e il binding.

2. Arrow function in render – sbagliato

Anche questo pattern soffre degli stessi identici problemi di quello precedente, a cambiare è solo la sintassi che per gli amanti del ‘bleeding edge’ (come me) troveranno più bella da vedere in quanto meno verbosa. Seguendo gli esempi:

class SayHello extends React.Component {
  constructor(props) {
    super(props);
    this.state = {message: 'Hello!'};
  }

  handleClick() {
    alert(this.state.message);
  }

  render() {
    return (
      // Qui il codice interessato
      <button onClick={() => this.handleClick()}>
        Say hello
      </button>
    );
  }
}

3. Binding nel costruttore – Passabile

E’ ciò che abbiamo fatto nel primissimo esempio:

class SayHello extends React.Component {
  constructor(props) {
    super(props);
    this.state = {message: 'Hello!'};
    
    // Binding necessario affinché all'interno del button, nel render, il this punti al contesto giusto.
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    alert(this.state.message);
  }

  render() {
    return (
      <button onClick={this.handleClick}>
        Say hello
      </button>
    );
  }
}

Converrete con me che il bind in questo modo sarà eseguito solo all’istanziazione del componente, il che è accettabile.
Unica pecca è che dovremo listare tutti i metodi della nostra classe all’interno del costruttore e chiamare il bind manualmente per ognuno di loro… possiamo fare meglio.

4. Arrow function per metodi di classe-Ottimo

Arriviamo infine all’ultimo pattern, quello che mi auguro inizierete in definitiva ad utilizzare dopo aver letto questo articolo.
Prendendo il solito esempio, vediamo come risulta applicando questo pattern:

class SayHello extends React.Component {
  constructor(props) {
    super(props);
    this.state = {message: 'Hello!'};
  }

  // Non effettuiamo più binding manuale attraverso il metodo .bind(), ma grazie ad un plugin per Babel
  // e alle arrow function, riusciamo a definire una proprietà della nostra classe che contenga
  // un metodo anonimo definito attraverso arrow function e quindi con il this del valore dello scope esterno (ovvero della classe stessa)
  handleClick = () => {
    alert(this.state.message);
  }

  render() {
    return (
      <button onClick={this.handleClick}>
        Say hello
      </button>
    );
  }
}

Da notare bene quanto scritto nei commenti. Difatti per utilizzare questa notazione, c’è bisogno di un plugin di Babel che ci permetta di definire delle proprietà all’interno di una classe ES6.
Se listate le devDependencies del vostro progetto react-native (non CRNA/Expo) vedrete apparire babel-preset-react-native. Questo preset, definito dal team di react-native stesso, potete visionarlo in questa repo e listando anche qui le dependencies vediamo come includano il class properties transform.
Quindi nel caso di react-native ci viene gratuito, ma se non vado errato anche CRA (create-react-app) per un’app da browser dovrebbe importare questo transform.

Ad ogni modo, in questa proprietà memorizziamo una funzione anonima definita attraverso una af che ci permette di ottenere il valore del this pari al valore del this esterno (della classe quindi).
Cosi facendo, chiamandola dall’onClick, il metodo è bello e funzionante e viene creato soltanto nel momento dell’istanziazione del componente.

Extra

parametri

Attenzione: Molti blog, articoli su medium e developer credono che il pattern numero 4 non possa sostituire totalmente il binding pensando di non poter passare i parametri utilizzando questo pattern… ma la functional programming comes to the rescue 🙂

Attraverso la tecnica del currying (strettamente collegata alla partial function application nel nostro caso) è possibile difatti passare anche parametri utilizzando il pattern numero 4 che spero, da oggi in poi, diverrà il vostro standard! Prendendo sempre l’esempio precedente, vediamo come:

class SayHello extends React.Component {
  constructor(props) {
    super(props);
    this.state = {message: 'Hello!'};
  }

  // Non effettuiamo più binding manuale attraverso il metodo .bind(), ma grazie ad un plugin per Babel
  // e alle arrow function, riusciamo a definire una proprietà della nostra classe che contenga
  // un metodo anonimo definito attraverso arrow function e quindi con il this del valore dello scope esterno (ovvero della classe stessa)
  handleClick = (string) => () => {
    alert(this.state.message+' '+string);
  }

  render() {
    return (
      <button onClick={this.handleClick('this is a long string')}>
        Say hello
      </button>
    );
  }
}

Come dicevo poco sopra, questa tecnica si definisce currying, nome che proviene da un certo Haskell B. Curry che un giorno si svegliò e disse:

Data una funzione con n parametri, questa potrà sempre essere suddivisa in n funzioni di 1 parametro ognuna;
in generale in p/p’ numero di funzioni dove p è il numero di parametri della funzione iniziale e p’ è il numero dei parametri delle nuove funzioni*

Attenzione: Un piccolo esercizio per voi che leggete: perché non ho direttamente definito il metodo handleClick nel seguente modo e cosa sarebbe successo se l’avessi fatto?
handleClick = (string) => { alert(this.state.message+' '+string); }

* supponendo che si voglia suddividere equamente i parametri
* formalizzata in maniera discorsiva da me

@autobind decorator

Da questo articolo del buon “vecchio” Dario, dovreste aver capito cos’è un decorator.
Ebbene grazie a un modulo per babel sviluppato da terzi, possiamo utilizzare il decorator @autobind su ogni metodo. Questa la repo del decorator dove potete vedere come installarlo e di seguito un piccolo esempio:

class MyClass extends Component {
  state = {isLoading: true}
  
  @autobind
  onChange() {}
  
  @autobind
  handleSubmit() {}
}

Attenzione: potrebbe venirvi voglia di utilizzare questo decorator a livello di classe piuttosto che per ogni metodo. Non è conveniente, è giusto fare il binding solo sui metodi per i quali effettivamente avete bisogno.

A proposito di me

Giacomo Cerquone

Appassionato di informatica sin da piccolo, cinefilo accanito, amante di tè e dei posti freddi. Focalizzato in full-stack JavaScript developing, contributor di diversi progetti open source e amante delle tecnologie bleeding edge nell'ambito della programmazione.

Gli articoli più letti

Articoli recenti

Commenti recenti