⇠ Torna alla Home

Pillole di React: Compound Component con Context API

30 ottobre 2018

Nel precedente tutorial sui Compound Component (ti consiglio la lettura prima di procedere avanti) ho introdotto il pattern e mostrato i principali vantaggi creando un componente d'esempio ImageGallery.

La tecnica proposta ha però mostrato anche alcuni limiti, in particolare l'impossibilità di innestare i componenti figli in una struttura annidata nel DOM del componente. Per funzionare correttamente i componenti dovevano essere figli diretti del Compound Component padre.

Come a breve vedremo, l'implementazione con l'ausilio delle Context API, non richiede una struttura markup rigida. Ciò significa che puoi nidificare i tuoi componenti figli nella struttura del DOM, senza alcun problema di comunicazione tra di essi. Sebbene non siano direttamente connessi, ad esempio in un rapporto padre / figlio, sono ancora in grado di comunicare.

Qui sotto un'anteprima del risultato che raggiungerò riscrivendo parte del codice visto nel precedente articolo:

Uso delle Context API

Dalla versione 16.3 React ha introdotto le Context API che permettono di passare dati attraverso un alberatura di figli, a prescindere che essi sia figli diretti o non.

Creare un context è molto semplice:

import React, { createContext } from 'react'

const MyContext = createContext();
const {Provider, Consumer} = MyContext;

Il contesto creato tramite il metodo createContext() è un oggetto che contiene una coppia di componenti: Provider e Consumer.

Il primo fornisce all'alberatura l'oggetto con i dati del contesto, il secondo permette di consumare i dati forniti dal Provider.

// Struttura tipico per il componente padre
class MyComponent extends React.Component {
  //...
  render() {
    //...
    // Uso del componente Provider. Richiede la prop value per il passaggio dell'oggetto con i dati del contesto
    return (
      <Provider
        value={contextObj}
      >
        {this.props.children}
      </Provider>
    );
  }
}
// Struttura tipica di un componente figlio
const ChildComponent = _ => (
  <Consumer>
    {contextObj => (
      //... Vista che consuma i dati dell'oggetto context
    )}
  </Consumer>
);

Utilizzo del Provider nel componente ImageGallery

Siamo padroni delle Context API, o quasi. Procediamo con il primo passo del nostro refactoring, dotando il componente ImageGallery del Context Provider.

Nella versione precedente, il componente si presentava così:

// Componente ImageGallery implementazione senza Context API
class ImageGallery extends React.Component {
  static Picture = Picture;
  static PreviewList = PreviewList;
  static PreviewListStatus = PreviewListStatus;
  state = {
    current: 0
  };
  clickPreviewHandler = index => () => {
    this.setState({ current: index });
  };
  render() {
    const { children, items } = this.props;
    const { current } = this.state;
    const { clickPreviewHandler } = this;
    const _children = React.Children.map(children, child => {
      let c;
      switch (child.type.displayName) {
        case "Picture":
          c = React.cloneElement(child, items[current]);
          break;
        case "PreviewList":
          c = React.cloneElement(child, {
            current,
            items,
            clickPreviewHandler
          });
          break;
        case "PreviewListStatus":
          c = React.cloneElement(child, {
            current,
            total: items.length
          });
          break;
        default:
          c = React.cloneElement(child);
      }
      return c;
    });
    return <div className="ImageGallery">{_children}</div>;
  }
}

Riscrivo il componente con le Context API:

// Implementazione componente ImageGallery con l'ausilio delle Context API
import React, { createContext } from "react";
//...
 
// Creiamo il context 
const GalleryContext = createContext();
const { Provider, Consumer } = GalleryContext;

class ImageGallery extends React.Component {
  static Picture = Picture;
  static PreviewList = PreviewList;
  static PreviewListStatus = PreviewListStatus;
  state = {
    current: 0
  };
  clickPreviewHandler = index => () => {
    this.setState({ current: index });
  };
  render() {
    const { children, items } = this.props;
    const { current } = this.state;
    const { clickPreviewHandler } = this;
      
    // Utilizziamo il componente Provider per impostare i dati "globali" per il componente e i suoi figli
    return (
      <Provider
        value={{
          currentItem: items[current],
          current,
          items,
          clickPreviewHandler,
          total: items.length
        }}
      >
        <div className="ImageGallery">{children}</div>
      </Provider>
    );
  }
}

Rispetto alla precedente, nella nuova implementazione NON occorre utilizzare:

  • React.Childrem.map per scorrere i figli;
  • React.cloneElement per clonare i figli passando le props occorrenti;
  • uno switch o costrutti if per identificare i figli.

Il Provider fornisce indistintamente, all'intero sotto albero del DOM, i dati passati nella proprietà value.

Il codice riscritto si presenta più snello e leggibile. L'eventuale aggiunta di un nuovo componente figlio lascerà, a meno dell'esigenza di nuovi dati nel context, invariata l'implementazione del componente padre. Nella versione precedente avremmo dovuto prevedere un nuovo case per gestire il nuovo componente figlio.

Utilizzo del Consumer nei componenti figli

I figli che ho creato per il componente ImageGallery sono tutti espressi come componenti funzionali stateless. Quello che farò a breve, è mostrarti come sia semplice reperire i dati del Context all'interno dei componenti figli che ne vogliono far uso.

Prendiamone uno come esempio per mostrare l'utilizzo del Context Consumer.

// Componente PreviewListStatus implementazione senza Context API
const PreviewListStatus = ({ current, total }) => (
  <div className="PreviewListStatus">
    {current + 1} / {total}
  </div>
);
PreviewListStatus.displayName = "PreviewListStatus";

Riscrivo il componente:

// Implementazione componente PreviewListStatus con l'ausilio delle Context API
const PreviewListStatus = _ => (
  <Consumer>
    {({ current, total }) => (
      <div className="PreviewListStatus">
        {current + 1} / {total}
      </div>
    )}
  </Consumer>
);

Come puoi notare, il componente Consumer fornisce l'accesso ai dati del contesto. Le proprietà current e total sono state precedentemente impostate nel Provider sul componente padre (ImageGallery).

Semplice no?

Nota che la precedente assegnazione della proprietà displayName diviene ora di nessuna utilità pratica. Possiamo quindi rimuoverla.

Conclusioni

Il componente aggiornato è realmente molto flessibile. L'utilizzo delle Context API mi permette di utilizzare e disporre i sotto componenti a mio piacimento e di creare una struttura di layout anche complessa, all'interno della quale nidificarli.

Per renderti l'idea, creo un div nel quale racchiudo il componente PreviewListStatus.

<ImageGallery items={galleryData}>
  <ImageGallery.Picture />
  <div style={{ border: "2px solid red", margin: "10px 0" }}>
    <ImageGallery.PreviewListStatus />
  </div>
  <ImageGallery.PreviewList />
</ImageGallery>

Il componente continua a funzionare a differenza di quanto succedeva nell'implementazione mostrata nel precedente tutorial.

Se vuoi sperimentare con l'esempio finito, non ti resta che modificarlo su codesandbox:

Edit Pillole di React: Componente ImageGallery tramite Compound Component e Context API

Sperando che il tutorial ti possa essere stato di aiuto ed ispirazione, ti lascio immaginare le possibilità che questo pattern può regalarti. Molto probabilmente, starai già pensando di aggiornare la tua libreria di componenti!

⇠ Torna alla Home