Patrones de Reutilización de Código entre Componentes de ReactJS

Reconozco que cuando empecé a jugar con ReactJS una de las cosas que menos me gustó fue el uso de JSX. Mezclar pseudo-HTML con el código Javascript me parecía sucio y tener que extender el lenguaje para ello, aun más. Reconozco también que, en aquel momento, se me pasó por alto la que quizá sea la característica más importante de JSX: que se trata de objetos Javacript, ni más, ni menos.

Tiene su gracia, sobre todo porque un año antes cuando jugaba con Hiccup para generar HTML desde Clojure ya decía esto:

Aquí se aprecia una de las ventajas de usar el mismo lenguaje para las plantillas que en el resto de la aplicación: podemos usar cualquier construcción del lenguaje dentro de las plantillas.

Poder aprovechar toda la potencia del lenguaje “host” para generar plantillas permite muchas alternativas a la hora de estructurar nuestro código para hacerlo más legible y reutilizable.

En este post vamos a ver algunas de las formas más habituales de conseguir esto en ReactJS a través de ejemplos muy sencillos.

OJO: Como siempre, la mejor opción es aislar al máximo la lógica de tu aplicación de las librerías que estés utilizando. Si puedes extraer la lógica que quieres reutilizar a funciones, clases o módulos independientes de ReactJS, esa es la mejor salida. Las técnicas que vamos a ver en este post están pensadas para escenarios en los que necesariamente tienes que estar tratando con componentes de ReactJS.

Crear componentes específicos

Cualquier persona que haya leído algo sobre ReactJS conoce esta idea. Si tengo varios componentes que forman una unidad “lógica”, puedo extraerlos a su propio componente para que quede más claro cuál es su misión y de qué forma se utilizan.

Si tengo un componente en el que parte de su render se dedica a mostrar una cabecera:

class MyComponent extends React.Component {
  render() {
    return <div>
             <header>
               <h1>Resultados</h1>
               <img src='/img/search-results.png'/>
             </header>           
             <div>...</div>
           </div>;
  }
}

Puedo extraer la construcción de la cabecera a su propio componente para abstraer los detalles y facilitar la legibilidad y la reutilización:

class Header extends React.Component {
  render() {
    return <header>
             <h1>{this.props.text}</h1>
             <img src={this.props.icon}/>
           </header>;
  }
}

class MyComponent extends React.Component {
  render() {
    return <div>
             <Header text='Resultados' icon='/img/search-results.png'/>
             <div>...</div>
           </div>;
  }
}

Cuando se trata de componentes sin estado, stateless components, podemos implementarlos directamente como una función que recibe un objecto props y devuelve el JSX correspondiente:

const Header = ({text, icon}) => <header>
  <h1>{text}</h1>
  <img src={icon}/>
</header>

Esta es la forma más básica de reutilizar código y la verdad es que no es muy espectacular. Cualquier librería medio decente permite hacer cosas similares (aunque probablemente no tan limpias como se hace con los componentes sin estado).

Generar componentes desde funciones

Puesto que los componentes de ReactJS son objetos normales y corrientes, podemos utilizar funciones formales y corrientes para construirlos. El caso más típico, que encontramos en casi todos los tutoriales de ReactJS, es utilizar un map para construir una colección de componentes:

render() {
  const contacts = this.props.contacts
    .map(c => <Contact name={c.name} email={c.email}/>);

  return <Contacts>{contacts}</Contacts>;
}

Visto así, no parece gran cosa porque cualquier sistema de plantillas tiene un for, repeat o similar para aplicar una plantilla a una colección de objetos, pero en ReactJS podemos hacer cosas más interesantes.

Por ejemplo, podemos fijar parte de los parámetros de un componente para evitar repetirnos y facilitar su uso:

// Simplificamos el uso del componente Header del ejemplo anterior
// para no tener que indicar la ruta del icono y homogeneizar los
// textos de los títulos
function title(text, icon) {
  text = 'My App: ' + text;
  icon = icon ? '/img/' + icon + '.png' || '/img/default.png';
  return <Header text={text} icon={icon}/>
}

const header = title('Resultados', 'search-results');

También podemos crear fácilmente factorías de componentes:

function createChart(type, title, data) {
  switch (type) {
    case 'bar': return <BarChart title={title} data={data}/>
    case 'pie': return <PieChart title={title} data={data}/>
    case 'line': return <LineChart title={title} data={data}/>
  }
}

// Cambiamos el tipo de gráfico dependiendo del estado actual
// del componente
const chart = createChart(this.state.chartType, title, data);
return <div>
  <h2>Some Chart</h2>
  {chart}
</div>;

Con esta técnica se puede resolver cualquier problema que te puedas encontrar, aunque hay ocasiones en que el resultado no es el más cómodo o legible, por lo que no está de más conocer las siguientes técnicas.

Encapsular componentes en otros componentes

A veces necesitas un componente cuya misión es servir como contenedor de otros componentes. Un caso habitual es un componente que actúa como panel, pestaña, tarjeta o algún concepto similar. Aunque puedes hacerlo usando funciones, es más sencillo aprovechar la sintaxis de JSX para hacerlo:

class Card extends React.Component {
  render() {
    <div className='card-component'>
      <div className='close-button'>×</div>
      
      {this.props.children}
    </div>
  }
}

const contactCard = <Card>
                      <div>Name: {this.props.name}</div>
                      <div>Email: {this.props.email}</div>
                    </Card

Una cosa a tener en cuenta es que no tenemos que limitarnos a componentes que añadan elementos HTML, y podemos tener componentes que encapsulan otro componente para añadirle algún tipo de funcionalidad.

Por ejemplo, podemos tener un componente que se encargue de proporcionar datos al componente que encapsula:

class Loader extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      loading: true,
      data: null
    }
  }
  componentDidMount() {
    this.props.load().then(data => {
      this.setState({
        loading: false,
        data
      });
    });
  }
  render() {
  
    if (this.state.loading) {
      return <LoadIndicator />;
    }
  
    let child = React.Children.only(this.props.children); 
    child = React.cloneElement(child, {data: this.state.data});
    return {child};
  }
}

// Para usarlo, encapsulamos el componente que necesita los datos,
// en este caso CustomerList dentro de un Loader, y éste se
// encargará de pasarle los datos a través del objeto props
<Loader load={loadCustomers}>
  <CustomerList/>
</Loader>

Herencia entre componentes

Es una de las alternativas que menos me gustan, en parte porque la herencia supone un acoplamiento muy fuerte, pero sobre todo porque Javascript no está pensado para este tipo de herencia y podemos acabar en casos un poco extraños. Aun así, tiene su utilidad y permite resolver algunos de los escenarios que antes resolvíamos con mixins.

Por ejemplo, si tenemos componentes que pueden reaccionar a timers podríamos crear una clase base encargada de gestionar los timers en el momento de desmontar el componente:

class TimedComponent extends React.Component {

  constructor() {
    this.timeouts = [];
    this.intervals = [];
  }
  
  setTimeout(callback, delay) {
    this.timeouts.push(window.setTimeout(callback, delay));
  }
  
  setInverval(callback, delay) {
    this.intervals.push(window.setInterval(callback, delay));
  }
  
  componentWillUnmount() {
    this.intervals.forEach(window.clearInterval);
    this.timeouts.forEach(window.clearTimeout);
  }
}

// Componente que necesita usar timers 
class Timeline extends TimedComponent {
  componentDidMount() {
    this.setInterval(this.refreshTweets, 30 * 1000);
  }
  refreshTweets() {
    ...
  }
}

Por desgracia, la mayoría de los casos que he visto son parecidos al ejemplo que acabo de poner, donde se utiliza la clase base para añadir métodos de utilidad, lo que es algo cuestionable si entiendes la herencia como una relación de “es un”.

Además tiene la clara limitación de que sólo podemos heredar de una clase, por lo que este mecanismo no escala demasiado bien (aunque tiene sus usos).

Componentes de orden superior

Los componentes de orden superior son otra forma de reutilizar lógica entre componentes de ReactJS. También nos permite resolver algunos de los escenarios que resolvíamos con mixins, y se basan en la idea de que las clases en Javascript no son más que funciones constructores.

Puesto que una clase es una función, y en Javascript las funciones pueden usarse como parámetros y valores de retorno de otras funciones, podemos crear clases al vuelo a partir de otras clases.

Hace poco escribí una explicación más detallada de cómo funcionan los componentes de orden superior en ReactJS, en la que mostraba este ejemplo:

function addLabel(Control) {
  return class extends ReactJS.Component {
    render() {
      return <label>
               {this.props.label}
               <Control {...this.props}/>
             </label>;
    }
  }
}

const LabeledTextBox = addLabel(TextBox);
const LabeledDateTime = addLabel(DateTimePicker);

<LabeledTextBox label='Nombre:' onChange={this.updateName}/>
<LabeledDateTime label='Fecha:' onChange={this.updateDate}/>

La función addLabel se encarga de recibir un componente y crear una nueva clase de componentes que encapsula ese componente añadiéndole un label. Así, a partir de componentes que ya tuviéramos creados, como un TextBox o un DateTimePicker podemos generar nuevos tipos de componentes.

Una ventana de este método frente al de herencia es que podemos componer varias funciones para crear nuestra nueva clase de componentes, por lo que no estamos limitados a añadir una única funcionalidad. Por ejemplo, podríamos tener funciones para añadir setInterval y para hacer la carga de datos, y usarlos así:


const addSetInterval = function(Component) {...};
const addDataLoader = function(Component, loadData) {...};

const MyEnhancedComponent = addSetInterval(addDataLoader(MyComponent));

Resumen

El hecho de que ReactJS utilice Javascript para todo, incluida la generación del HTML, en lugar de emplear un sistema de templates basado en strings como hacen otras librerías, permite aprovechar todas las características de Javascript para organizar el código de nuestra capa de presentación.

En este post hemos visto unas cuantas formas en que podemos abstraer fragmentos de funcionalidad para hacer nuestro código más legible o reutilizable. Cada una de ellas ofrece ciertas ventajas, por lo que es habitual mezclarlas todas dentro de una misma aplicación en función de cada escenario.

Aun así, no hay que olvidar que la mejor forma de reutilizar código cuando usamos cualquier librería es haciendo ese código independiente de ella, por lo que antes de plantearte de qué forma puedes crear componentes de ReactJS para reutilizar código, piensa si es posible hacerlo con funciones u objetos de Javascript completamente desacoplados de ReactJS.


Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos necesarios están marcados *

*

Puedes usar las siguientes etiquetas y atributos HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>