Componentes reusables con el patrón contenedor-contenido

Una de las claves del éxito de una buena arquitectura de ReactJS es potenciar al máximo posible la reutilización de los componentes. En este post voy a explicar las ventajas de usar el "patrón" contenedor-contenido para ese fin.

El patrón contenedor-contenido consiste, simplemente, en dividir el componente en dos: un componente se encarga de obtener la información -el contenedor- y otro componente muestra la información que se le pasa a través de sus propiedades -el contenido. El punto crucial es, por lo tanto, tener bien clara la distinción entre los conceptos de adquisición de datos, por un lado, y presentación de los datos, por otro. Es importante aclarar que, cuando hablo de presentación de datos, quiero decir que para el componente contenido no existe más información que la pasada por propiedades. Es más, no cuenta con ningún tipo de mecanismo para adquirir información nueva. Yo intento pensar en el componente contenido como una fotografía de nuestros datos en un momento dado. Así, no será hasta que el componente contenedor actualice su estado y nos ofrezca nuevos datos (por propiedades) que podremos mostrar información nueva.

Ejemplo práctico

Para tratar de ilustrar el patrón contenedor-contenido vamos a llevar a cabo un pequeño ejercicio práctico. El ejercicio consiste en desarrollar un componente que sea capaz de pintar en patalla la información relativa a un usuario (avatar y nickname). Para demostrar la capacidad de reutilización de este patrón en todo su esplendor, mostraremos información de un usuario de Github y otro de Random User.

Empezaremos creando el componente contenido. Como se ha dicho antes, todos los datos que necesita para pintarse han de serle pasados mediante propiedades:

// Componente contenido

import React from 'react';

export default class ContentComponent extends React.Component {  
  render() {
    return (
      <div className='cv-ContentComponent'>
        <img className='cv-ContentComponent-image' src={this.props.avatar} />
        <ul className='cv-ContentComponent-list'>
          <li className='cv-ContentComponent-nick'>{`Nick: ${this.props.username}`}</li>
        </ul>
      </div>
    );
  }
}

ContentComponent.propTypes = {  
  avatar: React.PropTypes.string.isRequired,
  username: React.PropTypes.string.isRequired
};

Toda la información que se va a presentar vendrá de sus propiedades, por lo que el origen de datos le es totalmente desconocido.

Por otra parte, tenemos el componente contenedor. Este lo sabe absolutamente todo sobre el origen de los datos que se quieren mostrar. Sin embargo, no tiene ni idea de cómo mostrar los datos una vez los ha adquirido. Para ello, cuenta con el componente contenido:

// Contenedor para Github

import React from 'react';  
import {ContentComponent} from '../src';

export default class GithubUserContainer extends React.Component {

  constructor(props){
    super(props);
    this.state = {}
  }

  componentDidMount(){
    fetch('https://api.github.com/search/users?q=carlosvillu')
    .then(response => response.json())
    .then(results => results.items[0])
    .then(profile => this.setState({username: profile.login, avatar: profile.avatar_url}))
  }
  render(){
    return <ContentComponent {...this.state} />
  }
}

Lo que hacemos en este componente es usar nuestro componente contenido para mostar los datos en el método render. Pero somos nosotros los que tenemos la lógica para ir a buscar esos datos. Eso lo hacemos en el método componentDidMount, pero perfectametne podría haber sido algo más complejo como interacturar con un Store de Flux o cualquier estructura de datos que pudiéramos imaginar. Como no tenemos acoplada la adquisición de datos a su presentación, podemos hacer otro contenedor que tome los datos de Random User y utilice el mismo componente de contenido para presentarlos:

// Contenedor para Random User

import React from 'react';  
import {ContentComponent} from '../src';

export default class RandomUserContainer extends React.Component {

  constructor(props){
    super(props);
    this.state = {}
  }

  componentDidMount(){
    fetch('http://api.randomuser.me')
    .then(response => response.json())
    .then(data => data.results[0].user)
    .then(profile => this.setState({username: profile.username, avatar: profile.picture.medium}))
  }
  render(){
    return <ContentComponent {...this.state} />
  }
}

En el ejemplo se ve cómo obtenemos los datos y cómo estos son procesados de forma distinta que en el caso de Github, pero al final producen el mismo output en forma de objeto state del componente.

Consejos prácticos

Con el uso habitual, verás que este patrón permite hacer cualquier manipulación de los datos dentro del componente contenido, siempre que estos sean tratados como una "caja negra"; es decir, que en ningún caso requieran la participación del componente contenedor para llevar a cabo esas manipulaciones. También es perfectamente válido incluir en el componente contenido lógica que manipule su estado interno, con el objetivo de que pueda mejorar la experiencia de usabilidad. Por ejemplo, si se trata de mostar una lista, podríamos permitir navegar y seleccionar elementos de la lista mediante el cursor. La regla que nunca hemos de romper es la de requerir un contenedor para llevar a cabo cualquier acción dentro del componente contenido.

La clave para lograr la máxima reutilización de de los componentes es mantener contenido y contenedor bien separados.

Bonus

Para profundizar un poco más sobre el tema, lee este post de @learnreact o mira el vídeo original de la conferencia de ReactJS.

Os dejo el repositorio con el código que he usado en este post y un enlace con el código funcionando.

Si te ha gustado este post, difunde la palabra. Tampoco dudes en dejar comentarios u observaciones. ¡Gracias! :)

Suscríbete a mi lista de correo

* Campos obligatorios