Un mixin para carga asíncrona de datos en ReactJS

Los mixins son para mi una especie de bicho mítico, tal vez porque no los he usado mucho o porque me cuesta sacarles partido, pero lo cierto es que me resultan atractivos (seguramente más de lo que deberían) y de hecho empecé este blog hablando de mixins en C#.

Con el uso que le he dado últimamente a ReactJS han vuelto a rondarme por la cabeza y creo que empiezo a ver más casos de aplicación que la mera herencia de implementación que me gustó hace tiempo cuando hablé de mixins en javascript.

Recordando qué es un mixin

Los mixin son un sistema para reutilizar código y extender clases u objetos existentes añadiéndoles (o mezclándoles, de ahí el nombre) la funcionalidad de otros.

Podemos verlos como un mecanismo a medio camino entre la herencia y la composición, y quizá la forma más simple de entenderlos sería pensar en herencia de implementación. Al incluir el mixin en una clase, ganamos acceso a las funciones y propiedades expuestas con el mixin.

En los lenguajes estáticos, esto es más limitado porque necesitamos conocer los tipos a priori y tenemos más restricciones a la hora de invocar métodos en un objeto si no conocemos su tipo real. Eso hace que se usen otras técnicas, exponiendo interfaces entre mixins y objetos que los reciben, aplicando patrones como bridges y adapters, y… en fin, haciéndolo todo mucho más seguro y menos divertido.

En cambio, en un lenguaje dinámico, donde podemos hacer lo que queramos tanto desde el punto de vista del mixin como desde el punto de vista del cliente del objeto que recibe el mixin, la cosa se vuelve más interesante.

El uso de mixins en ReactJS

Utilizando la forma clásica de definir componentes de ReactJS es fácil añadir mixins a un componente (con el estilo basado en clases de ECMAScript 2015 de momento no se puede):

var TickTock = React.createComponent({
  mixins: [SetInvervalMixin],

  // Resto del componente...
});

En el ejemplo, basado en la documentación de ReactJS sobre mixins, vemos que para añadir mixins a un componente sólo necesitamos indicarlo dentro de la propiedad mixins del objeto con la definición del componente.

Un mixin no tiene nada de especial, es sólo un objeto (sirve cualquier objeto) cuyas propiedades serán «mezcladas» con las de nuestro componente. Si el objeto incluye una propiedad color, desde nuestro componente podremos acceder a ella usando this.color. Hasta aquí, nada especialmente reseñable. Es muy parecido a lo que podemos hacer con Object.assign de ECMAScript 2015 o su equivalente de multitud de librerías.

La particularidad de los mixins en ReactJS es la forma en que interactúan con el ciclo de vida del componente. En un componente de ReactJS existen varios métodos que son invocados en distintos momentos de su ciclo de vida (al montarlo sobre el DOM, cuando se actualiza, cuando se va desmontar, etc.), y los mixins pueden «engancharse» a esos métodos sin sobreescribir lo que hayamos puesto en el propio objeto:

var LogMixin = {
  componentDidMount: function() {
    console.log('mixin did mount!');
  }
};

var Component = React.createClass({
  mixins: [LogMixin],
  componentDidMount: function() {
    console.log('component did mount!');
  },
  render: function() {
    return <div/>
  }
});

React.renderComponent(<Component />, document.body);
// En consola veríamos:
//   mixin did mount!
//   component did mount!

El orden en que se invocan los métodos es el orden en que están definidos los mixins y, por último, el método del componente.

Un mixin para cargar datos de forma asíncrona

Vamos a ver un ejemplo un poco más complejo (y útil) de mixin para permitir cargar datos de forma asíncrona en un componente.

Cuando en un componente necesitamos cargar datos externos, la forma recomendada es hacerlo en el evento componentDidMount, lanzando una petición ajax y, cuando ésta se complete, actualizando el estado. Esto presenta dos problemas:

  • Si el componente se ha desmontado antes de recibir el resultado, se generará un error cuando actualicemos el estado. Esto se puede evitar utilizando this.isMounted() antes de actualizar el estado.
  • Si cambias las propiedades del componente y nuestros datos externos depende de esas propiedades (algo discutible, pero relativamente normal), no nos basta con controlar el evento componentDidMount, tendremos que controlar también componentWillReceiveProps y cargar datos basándonos en el argumento nextProps que nos indica las próximas propiedades que vamos a recibir.

Nada de esto es terrible y se puede solucionar con un código similar a éste:

var Component = React.createClass({

  componentDidMount: function() {
    this.loadData(this.props);
  },

  componentWillReceiveProps: function(nextProps) {
    if (this.props.someValue !== nextProps.someValue) {
	  this.loadData(this.props);
	}
  },

  loadData: function(props) {
    // server.getData devuelve una promesa con el resultado
	// que queremos añadir al estado del componente
    server.getData(props.someValue).then(function(data) {
	  if (this.isMounted()) {
	    this.setState(data);
	  }
	});
  },

  // Resto del componente...
  
});

Como decía, no es taaaan malo, pero si tienes unos cuantos componentes que tienen que obtener datos externos, tener que replicar todo este código empieza a ser tedioso. Una solución bastante limpia es extraerlo a un mixin que nos permita reutilizarlo:

var FetchDataMixin = {

  componentDidMount: function() {
    this._loadData(this.props);
  },
  
  componentWillReceiveProps: function(nextProps) {
    if (this._shouldRefetch(nextProps) {
	  this.loadData(this.props);
	}
  },

  _shouldRefetch: function(nextProps) {
    // Si ha cambiado alguna de las propiedades indicadas en
	// this.fetchParams, hay que actualizar los datos cargados
  	return this.fetchParams.some(p => this.props[p] !== nextProps[p]);
  },
  
  _loadData: function(props) {
    // Actualizamos el estado a partir del valor de la promesa
	// devuelta por this.fetchData

	var args = this.fetchDataParams.map(p => props[p]);
	
    this.fetchData.apply(this, args).then(function(data) {
	  if (this.isMounted()) {
	    this.setState(data);
	  }
	});
  },
}

La idea es que el mixin gestione los eventos componentDidMount y componentWillReceiveProps, pero si os fijáis en el código original del componente que teníamos más arriba, veréis que hace falta también conocer la forma en que se cargan los datos para ese componente concreto (server.getData) y los valores que, cuando cambian, fuerzan la recarga de los datos (someValue).

Para hacer llegar esos valores al mixin podríamos usar una función que actuase como factoría de mixins, pero también podemos tirar por el camino dinámico (ya que nos ponemos, aprovechémoslo…) y hacer que el mixin asuma que en el objeto al que se «acoplará» existirán el método fetchData(prop1, prop2, ...) y la propiedad fetchParams con un array con las propiedades que fuerzan la recarga de datos, y que son las que habrá que pasar a la función fetchData.

Teniendo este mixin, el código de nuestro componente original quedaría así:

var Component = React.createClass({

  mixins: [FetchDataMixin],

  fetchParams: ['someValue'],  
  fetchData: server.getData,

  // Resto del componente...
});

Al definir el componente, además de añadir el mixin, debemos indicar las propiedades de props que necesitamos para actualizar la información y la función que usaremos para cargar la información. En este caso, como no necesitamos transformar nada, podemos usar directamente server.getData, pero también podríamos tener una función propia del componente que agregase información de varios servidores y devolviera una única promesa con el resultado (al estilo de un Promise.all).

El código se reduce bastante y, sobre todo, quitamos código que no aporta mucho para entender el funcionamiento del componente. Cuando tienes un proyecto con decenas de componentes que muestran datos externos (pensad en el típico dashboard), el ahorro es considerable.

Una pequeña advertencia

Los mixins de ReactJS son una buena manera de empaquetar funcionalidad reutilizable para añadirla a otros componentes. Al poder intervenir durante el ciclo de vida del objeto, tenemos bastante flexibilidad para realizar la integración entre el mixin y el componente al que se añade.

Una cosa con la que hay que tener cuidado es con el abuso de los mixins. He visto más de una vez mixins que añaden un par de métodos «de utilidad» a un componente, pero que no tienen nada que ver con el propio componente, no acceden a props, ni a state, ni siquiera a this. Para eso no hace falta utilizar un mixin, y las funciones normales son perfectamente válidas.

No intentes encajar todo en conceptos de la librería que estás usando y no te olvides de los conceptos que tiene el propio lenguaje.

5 comentarios en “Un mixin para carga asíncrona de datos en ReactJS

  1. Hola,

    Muchas gracias por tus artículos que me sirven tanto para aprender como para repasar conceptos. Tengo una pregunta, si tengo un mixins que usar en diferentes componentes (divididos en diferentes archivos .js/.jsx), ¿puedo crear un fichero .js independiente para los mixins y poder invocarlos en aquellos componentes en los cuales, uso dicho mixins?.

    No te veas obligado a contestarla si no quieres.

    Estoy aprendiendo React y para ello estoy con xampp (no estoy usando NodeJS).

    Muchas gracias y una saludo.

  2. Hola Juan,

    Puedes crear los mixins en un fichero y reutilizarlos en varios componentes, de hecho esa es la gracia ;-)

    Si estás empaquetando tu aplicación cliente con browserify, webpack, o similar, basta con hacer un require del fichero con el mixin en aquellos ficheros donde quieras usarlo.

    Si estás generando bundles desde el servidor, la idea es parecida pero depende de lo que te ofrezca cada plataforma. En el caso de xampp no tengo ni idea.

    De todas formas, aunque tu servidor no sea node, te recomiendo que lo uses para el desarrollo de la parte cliente porque las herramientas que ofrece están muy bien.

    Un saludo,

    Juanma

  3. Hola Juanma,

    Muchas gracias por tu explicación. La verdad es que estoy usando xampp (apache) porque estoy experimentando con la API de Blogger (y si me gusta como queda lo usaría en vez de la plantilla de Blogger) y Blogger no funciona en Node JS.

    La verdad es que hay mucha información para una arquitectura React + Node JS pero fuera de esta no encuentro nada. De todas formas es normal que se produzca esta situación, porque, a fin de cuentas, para usar algo con la arquitectura Apache tenemos PHP. Pero me gusta probar todas las alternativas en esto de la informática jajaja.

    Muchas gracias y un saludo.

  4. Juan, Juanma se refiere a utilizar Node sólo durante el desarrollo. Facilita herramientas magníficas para trabajar. Después, cuando lo subes al servidor, no necesitas Node, es todo JavaScript convencional en la parte cliente y el «framework» de «backend» que estés utilizando (PHP, ASP.NET, Ruby on Rails, etc).

  5. Hola Julián,

    Muchas gracias por la aclaración, la verdad es que no lo había pillado. Pues también es verdad, al browserify y webpack no dejan de ser gestores de paquetes que «traducen» el estándar ES6 (entre otras cosas).

    Debería de probarlo en el portátil estas vacaciones.

    Muchas gracias y un saludo.

Comentarios cerrados.