Especificación de tipos en ReactJS con propTypes

En las últimas semanas he dedicado bastante tiempo a desarrollar una aplicación real con ReactJS y eso me ha permitido ver cómo se comporta la librería en un proyecto más complejo que los típicos tutoriales.

Entre mixins, funciones ligadas al ciclo de vida del componente y demás particularidades de ReactJS (de las que seguro que acabaré escribiendo algo), hay una funcionalidad que me ha resultado especialmente útil: la validación de propiedades usando propTypes.

En este post vamos a ver qué es propTypes, pero además veremos cómo podemos mejorar lo que nos ofrece ReactJS de serie y qué otras ventajas nos aporta el uso de propTypes.

Pero vayamos por partes…

Qué es eso de propTypes

Cuando hace un tiempo explicaba cómo crear un componente en ReactJS, veíamos que necesitamos definir una función render que se encargase de devolver el (Virtual) DOM asociado al componente, y que el componente podía recibir información inmutable de su componente padre a través del objeto this.props. Si todo esto te suena a chino, puedes revisar cómo funciona ReactJS.

La propiedad propTypes podemos incluirla en el objeto que usamos para definir nuestro componente de ReactJS y nos permite establecer una serie de validaciones que se realizarán sobre las propiedades recibidas por el componente en el momento de su creación.

Podemos realizar validaciones de distinto tipo. Lo más sencillo es validar el tipo de la propiedad, por ejemplo si es un number, o un string o un array. Podemos también indicar si la propiedad es opcional o es obligatoria. Podemos definir la «forma» que tendrá la propiedad, es decir, podemos por ejemplo definir que será un objeto que contendrá una propiedad id de tipo number y una propiedad name de tipo string. Incluso podemos añadir una función de validación con lógica personalizada para validar la propiedad.

Como ejemplo, podríamos pensar en un componente que muestra los datos de un usuario (id, nombre y rol) y permite guardarlo o eliminarlo. El uso de ese componente sería algo así:

<UserEditor user={theUser} onSave={saveUser} onDelete={deleteUser} />

El componente recibe a través del objeto props los datos actuales del usuario y las funciones que deberá invocar para guardar y eliminar el usuario. Esto podemos validarlo de la siguiente forma:

var UserEditor = React.createClass({
  propTypes: {
    user: React.PropTypes.shape({
      id: React.PropTypes.number.isRequired,
      name: React.PropTypes.string.isRequired,
      role: React.PropTypes.oneOf(['admin', 'power user', 'user'])
    }).isRequired,
    onSave: React.PropTypes.func.isRequired,
    onDelete: React.PropTypes.func.isRequired
  }
});

En ese código se pueden ver unas cuantas de las opciones existentes para definir la validación de las propiedades, como el uso de shape para definir estructuras más complejas (el «tipo» user) o el oneOf para definir «tipos» enumerados. En la documentación de ReactJS.propTypes podéis ver todas las opciones existentes.

Aumentando el efecto de la validación

Con esto hemos definido el aspecto que deberían tener las propiedades que recibe un componente pero, ¿qué pasa cuando hay algún error?

Si estamos ejecutando la versión de producción de ReactJS, no pasa absolutamente nada. ReactJS desactiva la validación de propiedades cuando ejecutamos la versión de producción de la librería por motivos de rendimiento.

Si estamos usando la versión de desarrollo de ReactJS, cuando se produzca un error de validación se mostrará un warning en la consola. Sí, sólo eso, un triste warning en la consola de Javascript del navegador que, probablemente no lleguemos a ver.

Para mejorar esto y utilizar la validación de propiedades de una forma más cercana a lo que serían precondiciones en diseño por contrato podemos aprovecharnos de que Javacript es un lenguaje dinámico y aplicar un poco de monkey patching sobre el objeto console:

function setReactWarnsAsErrors() {
  var warn = window.console.warn;

  window.console.warn = function() {
    if (arguments && arguments[0].match(/(^Warning: )|(react)/)) {
      throw Error(arguments[0]);
    }
    warn.apply(window.console, arguments);
  };
}

window.onError = function(error) {
  window.alert('Unexpected Error: ' + error);
}

setReactWarnsAsErrors();

No es que sea una solución muy limpia (más bien todo lo contrario), pero al menos mientras estamos desarrollando los avisos nos saltarán como errores y podremos detectar problemas más fácilmente.

Ventajas adicionales

El uso de los propTypes tiene algunas ventajas adicionales que hace que sean especialemente interesantes desde mi punto de vista.

Por una parte, nos permite identificar el API del componente de una forma más cómoda.

Un problema que existe al definir un componente en ReactJS es que lo único obligatorio es definir su método render, y éste no recibe ningún parámetro. Esto hace que si queremos saber qué propiedades debemos pasarle al componente tengamos que leer todo el código del método render para saber qué propiedades son necesarias.

Al definir la validación de las propiedades con propTypes tendremos documentado en un punto todo lo que necesita el componente para trabajar y nos ayudará a utilizarlo en el futuro. Es una buena forma de ahorrarnos un comentario de documentación.

Por otro lado, en aplicaciones Javascript es muy frecuente que trabajemos con un modelo de datos que está definido de forma externa a nuestra aplicación. En lugar de definir mediante funciones contructoras los objetos con los que vamos a trabajar, estos objetos llegan serializados en JSON desde APIs externas y, si queremos saber el contenido de esos objetos tenemos que saltar a otro proyecto o a consultar la documentación del servicio que estamos consumiendo.

Podemos utilizar propTypes para documentar el modelo de objetos dentro de la aplicación Javascript, pero con la ventaja de que no hace falta realizar una conversión entre el objeto JSON y el objeto construido, ya que sólo definimos la forma, no el «tipo». Duck Typing en estado puro.

Además, podemos organizar la definición de estos objetos de manera que sea fácil de reutilizar en lugar de definirlos inline en cada componente. Así, en el ejemplo anterior del usuario podríamos tener:

// Fichero: AppTypes.js
var AppTypes = {};

AppTypes.role = React.PropTypes.oneOf(['admin', 'power user', 'user']);
AppTypes.user = React.PropTypes.shape({
  id: React.PropTypes.number.isRequired,
  name: React.PropTypes.string.isRequired,
  role: AppTypes.role.isRequired
});

// Fichero UserEditor.jsx
var UserEditor = React.createClass({
  propTypes: {
    user: AppTypes.user.isRequired,
    onSave: React.PropTypes.func.isRequired,
    onDelete: React.PropTypes.func.isRequired
  }
});

De esta forma, podemos organizar la definición de los «modelos» usados por los componentes de la manera que queramos, por ejemplo en un único fichero si la aplicación es lo bastante simple, y también podemos reutilizar esa definición de «tipos» en todos los componentes que vayan a trabajar con ellos.

Conclusiones

Hacer aplicaciones de juguete está muy bien para familiarizarse con una tecnología, pero hasta que no desarrollas una aplicación de un cierta complejidad, hay partes que no llegas a apreciar.

El uso de PropTypes no es algo a lo que se le dé mucha importancia en los típicos tutoriales y presentaciones sobre ReactJS, pero para nosotros ha sido extremadamente útil en la vida real.

Las ventajas que aporta en cuanto a validación de precondiciones, documentación del API de los componentes y documentación del modelo de la aplicación son lo bastante grandes como para que merezca la pena su uso.

Seguro que alguno de vosotros estará pensando que todo esto es necesario únicamente porque Javascript es un lenguaje de tipado dinámico. Tiene toda la razón del mundo. Si usásemos otro lenguaje con tipado estático y un sistema de tipos medianamente decente, todo esto no sería necesario (y tendríamos alguna ventaja más), pero esa es otra discusión distinta.