Foto Andrea Mangano
Andrea Mangano

Pillole di React: Render Prop Pattern

16 novembre 2018
9m
Se il nome "render prop" potrebbe trarre in confusione, i vantaggi forniti da queto design pattern sono molteplici. Questo tutorial tratta gli argomenti di maggiore interesse relativi alla creazione di un componente che utilizza il pattern "render prop" fornendoti linee guida e chiari esempi d'implementazione.
React.js
Render Rrop Rattern
Advanced Pattern
Tutorial
Condividi su

Finalità del tutorial

Il termine “render prop” è utilizzato per indicare una tecnica di sviluppo in React, attraverso la quale un componente condivide funzionalità e informazioni di stato con altri componenti figli. Questa modalità di scrittura componenti è ampiamente utilizzata in molte famose librerie React (react-router per citarne qualcuna) ed integrata anche nella recente versione delle Context API.

Un componente “render prop” accetta una funzione (come parametro) che viene richiamata dal componente stesso nella fase di render. Essa va a sostituire la logica interna della vista a favore di quella dettata dall’esterno.

Usando una prop per definire ciò che viene renderizzato, il componente inietta semplicemente la funzionalità e dati senza bisogno di sapere come essi vengono applicati all’interfaccia utente.

Se il concetto non ti è molto chiaro, niente paura!

Questo tutorial tratta gli argomenti di maggiore interesse relativi alla creazione di un componente che utilizza il pattern render prop fornendoti chiari esempi d’implementazione.

In particolare:

  • Creo un componente WindowInfo che visualizza le dimensioni attuali della finestra del browser (anche quando soggetta a ridimensionamento)
  • Riscrivo il componente affinché, i dati relativi alla misura di larghezza e altezza della finestra, possano essere agevolmente utilizzati da altri componenti, senza che essi siano hard-coded nel componente WindowInfo

Non manca nulla, iniziamo!

Il nocciolo del problema

Vado a creare il componente WindowInfo nella classica maniera che ti aspetti.

class WindowInfo extends React.Component {
  state = {
    width: window.innerWidth,
    height: window.innerHeight
  };

  handleResizedScreen = () => {
    this.setState({
      width: window.innerWidth,
      height: window.innerHeight
    });
  };
  componentDidMount() {
    window.addEventListener("resize", this.handleResizedScreen);
  }
  componentWillUnmount() {
    window.removeEventListener("resize", this.handleResizedScreen);
  }
  render() {
    const { width, height } = this.state;
    return (
      <div>
        Window size: {width} x {height}
      </div>
    );
  }
}

Creiamo un secondo componente che si occupa di calcolare e visualizzare il numero complessivo di pixel presenti nella finestra.

const PixelAmount = ({ width, height }) => (
  <div>Total pixels: {width * height}</div>
);

PixelAmount.PropTypes = {
  width: PropTypes.number.isRequired,
  height: PropTypes.number.isRequired
};

Questo componente necessità dei dati width e height presenti nello stato del componente WindowInfo.

La soluzione banale mi porta a renderizzare il componente PixelAmount dentro il componente WindowInfo. Come figlio può accedere liberamente alle proprietà dello stato e a qualsiasi metodo definito nel componente.

class WindowInfo extends React.Component {
  state = {
    width: window.innerWidth,
    height: window.innerHeight
  };

  handleResizedScreen = () => {...};             
  componentDidMount() {...}
  componentWillUnmount() {...}
  
  render() {
    const { width, height } = this.state;
    return (
      <div>
        Window size: {width} x {height}
        <PixelAmount width={width} height={height} />
      </div>
    );
  }
}

È evidente come la soluzione è limitante e rende impossibile ridistribuire la funzionalità di calcolo delle dimensioni della finestra, in diversi contesti dell’applicazione che ne potrebbero far uso.

Il problema, in definitiva, è che non posso accedere ai dati d’interesse (presenti dentro WindowInfo) dall’esterno del componente. Per intenderci, nella mia App, non posso utilizzare i componenti WindowInfo e PixelAmount come segue:

function App() {
  return (
    <div className="App">
      <WindowInfo />
      <PixelAmount width={width} height={height} />
    </div>
  );
}

Potendo, sarei libero di renderizzare il mio componente al di fuori di WindowInfo pur utilizzando i dati da esso forniti.

Component Composition con proprietà children

La dipendenza tra componente padre (WindowInfo) e il componente figlio (PixelAmount) è alla base del problema.

Per questa ragione potrei pensare di utilizzare la proprietà children per passare il componente PixelAmount dall’esterno, come figlio del componente WindowInfo.

function App() {
  return (
    <div className="App">
      <WindowInfo>
      	<PixelAmount width={width} height={height} />
      </WindowInfo>
    </div>
  );
}

Tale soluzione mi costringe a variare leggermente il codice del componente WindowInfo in modo da utilizzare la prop speciale children :

class WindowInfo extends React.Component {
  state = {...};
  handleResizedScreen = () => {...};             
  componentDidMount() {...}
  componentWillUnmount() {...}
  
  render() {
  	const {children} = this.props;
    const { width, height } = this.state;
    return (
      <div>
        {children} // Rimpiazza rendering con il contenuto della proprietà children
      </div>
    );
  }
}

Tuttavia, nemmeno questa è la soluzione. I parametri width e height non vengono letti dallo stato del componente WindowInfo.

La composizione del componente tramite la proprietà children, da sola non è sufficiente a risolvere il problema.

Render prop in soccorso

Questo è il contesto dove giunge in aiuto il pattern render prop che potenzia la composizione di Componenti tramite la definizione di una vera e propria funzione di rendering.

// Definizione componenti figli (children) tramite elementi React 
<MyParentComponent>
  <MyChildComponent />
</MyParentComponent>

// Definizione elementi figli (children) come funzione che li renderizza
<MyParentComponent>
  { () => <MyChildComponent /> }
</MyParentComponent>

La proprietà children non è più un elemento React ma una funzione. Piuttosto che passare i componenti come figli, passa una funzione che li renderizza.

Con riferimento all’esempio precedente, avremo:

<WindowInfo>
  { () => <PixelAmount width={width} height={height} /> }
</WindowInfo>

Problema risolto? Quasi.

Anche in questa modalità, il componente figlio PixelAmount non ha accesso allo scope interno del componente WindowInfo, con conseguente impossibilità di reperire i dati. Tuttavia, utilizzando una funzione per renderizzare i figli, è possibile passare dati e funzionalità interne al componente padre, attraverso gli argomenti stessi della funzione. Per intenderci:

<WindowInfo>
  {({width, height}) => <PixelAmount width={width} height={height} /> }
</WindowInfo>

Questo implica una piccola modifica al componente WindowInfo nella definizione della sua funzione di render. Poiché children è una funzione, la richiamo passando come argomenti i dati che intendo rendere fruibili dall’esterno.

class WindowInfo extends React.Component {
  state = {...};
  handleResizedScreen = () => {...};             
  componentDidMount() {...}
  componentWillUnmount() {...}
  
  render() {
  	const {children} = this.props;
    const { width, height } = this.state;
    // Essendo children una funzione, la richiamo passando come argomenti i dati che intendo fornire all'esterno.
    return children({width, height}) // Inoltre restituisco direttamente la funzione children, evitando l'extra <div/> che la conteneva, di base inutile.
  }
}

Il gioco è fatto, la flessibilità desiderata è presto raggiunta.

Il componente WindowInfo diventa un vero e proprio componente di utility, in grado di essere utilizzato agevolmente in contesti differenti senza costrizioni legate al markup.

Ad esempio, mi viene semplice costruire una vista dove visualizzo, oltre il totale pixel anche l’effettiva larghezza e altezza della finestra del browser, con un markup totalmente personalizzabile!

<WindowInfo>
  {({width, height}) => (
      <div>
      	<div>
          WindowSize: <strong>{width}</strong> x <strong>{height}</strong>
      	</div>
      	<PixelAmount width={width} height={height} />
      </div>
  )}
</WindowInfo>

Perché il nome “render prop”?

Inizialmente, non nascondo che me lo sono chiesto anch’io. Guardando l’esempio potresti non trovare un esplicito nesso con il nome, dato che non passiamo nessuna prop al componente WindowInfo, bensì una funzione come figlio.

Giusto. Una funzione che però rappresenta il valore di una prop speciale (children) e che definisce il modo in cui il componente è renderizzato.

Evitando giochi di pensiero e affinità logiche :), il nome risale all’iniziale modalità di implementazione del pattern. Nella pratica, veniva definita una prop con nome “render” a cui era assegnata una funzione che specificava, nello stesso modo che ti ho mostrato in precedenza, la vista del componente.

Con riferimento, all’esempio di WindowInfo, avremmo utilizzato il componente nel seguente modo:

<WindowInfo
  render={
    ({width, height}) => <PixelAmount width="width" height="height" />
  }
/>

All’interno del componente WindowInfo avremmo richiamato la prop render passando come argomenti width e height:

render() {
  const {render} = this.props;
  const { width, height } = this.state;
  return render({width, height}) // Richiamo la funzione passata nella prop "render"
}

Da notare che entrambe le modalità sono valide e che il nome del parametro “render” può essere assegnato in maniera arbitraria, in quanto non costituisce una prop speciale.

Componente Render Prop con “prop render” e “function as child”

È possibile dare la possibilità di utilizzare WindowInfo specificando la funzione di render come parametro o funzione figlio. Per far questo, devo prevederlo durante lo sviluppo del mio componente, ma lo sforzo necessario non richiede molto tempo extra.

Ti mostro di seguito un’implementazione semplicistica priva dei controlli di tipo sul parametro render e children (a rigore dovrei verificare che entrambi siano funzioni). Per la finalità che mi pongo, posso permettermi questa libertà!

class WindowInfo extends React.Component {
    
  state = {
    width: window.innerWidth,
    height: window.innerHeight
  };

  handleResizedScreen = () => {
    this.setState({
      width: window.innerWidth,
      height: window.innerHeight
    });
  };

  componentDidMount() {
    window.addEventListener("resize", this.handleResizedScreen);
  }

  componentWillUnmount() {
    window.removeEventListener("resize", this.handleResizedScreen);
  }

  render() {
    const { render, children } = this.props;
    const { width, height } = this.state;

    if (!render && !children) {
      throw new Error("Occorre definire una funzione di render come prop render o come funzione figlio.");
    }

    if (render) {
      return render({ width, height });
    }

    return children({ width, height });
  }
}

Essenzialmente controllo che sia definita la funzione di renderizzazione tramite render prop o funzione figlia, altrimenti lancio un errore che notifica all’utente la dimenticanza. Personalmente eviterei la possibilità di lasciare la possibilità di instanziare un componente WindowInfo senza che sia definita una funzione di rendering. Che senso avrebbe?

Secondariamente verifico che sia presente la prop “render” (se esiste richiamo la funzione definita nella prop render) o, se non presente, richiamo la funzione children.

In questo modo stabilisco una priorità maggiore alla proprietà render qualora siano presenti entrambe le definizioni. Qualora si desideri la priorità opposta, basta invertire l’ordine di verifica.

Eccoti l’esempio completo:

Considerazioni finali

Spero che quanto visto ti sia d’aiuto e possa, in qualche modo, agevolare lo sviluppo di componenti React rendendoti più consapevole delle possibilità e dei rischi. Sopratutto se sei impegnato nella creazione di una libreria di componenti, ti renderai presto conto di quanto le scelte di implementazione possano aumentare o diminuire la validità, la possibilità d’estensione e di utilizzo dei tuoi componenti nelle applicazioni reali.

La conoscenza dei pattern facilita la scrittura del codice, giustificando e indirizzando verso una scelta che sia più adatta alle attuali e future esigenze.

Alternativa al pattern render prop: HOC

Riguardo invece al pattern render prop nello specifico, un lettore più attento ed esperto potrebbe sottolineare che esso offre una soluzione molto simile al pattern HOC (Higher Order Component). Sicuramente lo è. Entrambi fungono da decoratori che forniscono funzionalità ai componenti che li utilizzano.

Tuttavia, la flessibilità nella composizione di componenti è differente: la natura stessa del pattern HOC preclude che tu possa costruire una vista in cui i componenti che utilizzano i dati forniti dall’HOC siano arbitrariamente innestati in un’alberatura del DOM.

Nuove prospettive con gli Hook React

La proposta degli Hook in React (https://reactjs.org/docs/hooks-intro.html) potrebbe concretizzarsi in un prossimo futuro nella versione 16.7. Malgrado l’introduzione degli Hook garantisca del tutto la legacy, essa porta con sé nuove visioni e nuovi pattern di sviluppo. Gli Hook nascono per risolvere in maniera più efficiente ed organizzata lo stesso problema per cui usiamo componenti render prop ed HOC.

Con gli Hook, puoi estrarre la logica da un componente in modo che possa essere testata indipendentemente e condivisa tra componenti. Essi consentono di riutilizzare la logica senza modificare le gerarchie dei componenti nel DOM di pagina.

Pillole di React: Compound Component con Context API

30 ottobre 2018
React.js
Compound Component
Context API
Advanced Design Pattern
Tutorial
L'autore
Foto di Andrea Mangano

Andrea Mangano

Frontend Developer @ iBuildings

Da più di otto anni, costruisco interfacce utente per siti e applicazioni web commerciali, con constante curiosità ed entusiasmo per le nuove tecnologie.

Professionalità, tenacia e propensione al rischio sono i miei tratti distintivi.

Seguimi su twitter: @andreaman87