Componentes de Alto Orden en React.js

Algo que ocurre muy seguido es que varios componentes de React vayan a necesitar usar una misma funcionalidad o extenderse con funciones de terceros (como el acceso a un Store o internacionalización).

Originalmente esto se lograba gracias a la utilización de Mixins, estos eran, básicamente, objetos que poseían los métodos que queríamos compartir, un ejemplo (de la misma documentación).

var SetIntervalMixin = {
componentWillMount() {
this.intervals = [];
},
setInterval() {
this.intervals.push(setInterval.apply(null, arguments));
},
componentWillUnmount() {
this.intervals.forEach(clearInterval);
}
};

Este mixin contiene la lógica para poder crear fácilmente un intervalo (como con window.setInterval) el cual se borre automáticamente al desmontarse el componente, evitándonos tener que pensar en hacer esto a mano y en causar problemas de memoria si nos olvidamos.


Resulta que desde que se incorporaron las clases como forma de crear componentes, y más aún con las funciones para componentes puros, ya no es posible usar mixins en React.js.

Mixins Are Dead. Long Live Composition por Dan Abramov

Las razones para esto fueron que ES2015 no tiene soporte a mixins de forma nativa por lo que en vez de crear una API propia decidieron no soportarlos. El hacer esto significó tener que buscar nuevas formas de extender componentes para agregar funcionalidades comunes.

La solución llego desde el lado de la programación funcional gracias a las Funciones de Alto Orden. Estas son funciones que reciben una o más funciones como argumentos y devuelven una nueva función.

En React esto se traslada a Componentes de Alto Orden. Haciendo un paralelismo, es una función que recibe uno o más componentes y devuelve uno nuevo. Veamos un ejemplo super básico.

import React from 'react';
function getViewport() {
return {
height: window.innerHeight,
width: window.innerWidth,
};
}
// nuestro componente de alto orden
function withViewport(WrappedComponent) {
return function WithViewport(props) {
return (
<WrappedComponent
getViewport={getViewport}
{...props}
/>
);
}
}
export default withViewport;

Como vemos en el ejemplo tenemos una función getViewport el cual nos devuelve un objeto con el width y height de nuestro navegador y nuestro HOC (High Order Component — Componente de Alto Orden) que recibe un componente y devuelve un nuevo componente puro, el cual a su vez le pasa los props que recibe al componente envuelto y adicionalmente la función getViewport.

Ahora nuestro WrappedComponent recibiría, además de sus props normales, una función llamada getViewport. De esta forma muy simple podemos empezar a extender la funcionalidad de nuestros componentes igual que hacíamos con los mixins. Volviendo al ejemplo anterior de mixins veamos ahora como haríamos eso mismo usando un HOC.

import React, { Component } from 'react';
function setIntervalHOC(WrappedComponent) {
return class WithSetInterval extends Component {
componentWillMount() {
this.intervals = [];
}
setInterval(..args) {
const id = setInterval.apply(null, args);
this.intervals.push(id);
return id;
}
componentWillUnmount() {
this.intervals.forEach(clearInterval);
}
render() {
return (
<WrappedComponent
setInterval={this.setInterval.bind(this)}
{...this.props}
/>
);
}
}
}

Esa es la versión HOC del mixin para usar setInterval, la diferencia es que ahora es una función que recibe un componente y lo renderiza pasándole el setInterval propio, y es el componente WithSetInterval el cual posee la lista de intervalos y se encarga de borrarlos al desmontarse.

De esta forma el componente envuelto solo sabe que si llama props.setInterval va a crear un intervalo y que automáticamente se va a borrar al desmontarse el componente. Veamos por último como lo usaríamos:

// ./components/App.jsx
import React, { Component } from 'react';
import setIntervalHOC from './decorators/set-interval.js';
class App extends Component {
static propTypes = {
timer: PropTypes.number,
}
static defaultProps = {
timer: 500,
}
state = {
amount: 0,
}
componentDidMount() {
this.props.setInterval(
this.tick.bind(this),
this.props.timer
);
}
tick() {
this.setState({
amount: this.state.amount + 1,
});
}
render() {
return (<div>{this.state.amount}</div>);
}
}
export default setIntervalHOC(App);
// ./index.js
import React from 'react';
import { render } from 'react-dom';
import App from './components/App.jsx';
render(
<App timer={1000} />,
document.getElementById('app')
);

Como vemos simplemente creamos nuestro componente que haga uso del intervalo y se exporte envuelto en setIntervalHOC, de esa forma cuando importemos App vamos a importar en realidad el componente devuelto por el HOC y al renderizarse mostraría primero un 0, luego de un segundo un 1, y así, cada segundo (o lo que hayamos indicado a la propiedad timer de App, o si no indicamos nada 500ms),iría aumentando hasta que se desmonte.

Gist con el ejemplo:
https://gist.github.com/sergiodxa/09fa274d68c929a4059bdb8000c03e49

Conclusión

Los Componentes de Alto Orden son una forma excelente, y fácil de usar, para extender componentes. Este patrón es usado por ejemplo en React Redux para conectar un componente al Store de Redux.

Hay otras formas de usarlos además de la vista acá, el patrón que usamos se conoce como PropsProxy ya que estamos manipulando los props que llega al componente. Otro patrón es Inheritance Inversion que consiste en devolver un nuevo componente que extienda el componente que estamos envolviendo.

Como detalle extra, los HOC se pueden usar como decoradores siguiendo la propuesta actual (y capaz obsoleta), permitiendo usarlos de esta forma:

@setIntervalHOC
class App extends Component { ... }
export default App;