Entendiendo cómo funciona JSX

El JSX usado por ReactJS para generar su Virtual DOM es una de las características de ReactJS que más chocan al principio y que más rechazo suelen generar. Eso de incluir «pseudo html» dentro de ficheros javascript no suele gustar. Por cierto, resulta curioso que a la inversa, es decir, incluir «pseudo javascript» en ficheros html, como se hace en casi todos los demás sistemas de templates, esté más aceptado.

Independientemente de que te guste mucho o poco, lo cierto es que la idea de utilizar JSX para generar un Virtual DOM que luego se sincronice con el DOM real se ha extendido a otras librerías y frameworks, como Cycle.js o Inferno. Incluso hay lenguajes, como TypeScript, que ofrecen soporte de forma nativa para esta sintaxis (lo que me parece un error, pero eso es otro tema).

Si estás trabajando con este tipo de librerías y eres de los que se preocupan por comprender en qué se basan las abstracciones que están utilizando, este post te puede ayudar a conocer mejor cómo funciona JSX y cómo afecta eso a tu aplicación.

De JSX a generar HTML

A poco que hayas leído algo sobre ReactJS, seguro que te suena la conversión que se hace para pasar de JSX a código Javascript puro. Convierte código JSX con este aspecto:

<Panel title='Opciones' color='green'>
		Texto del panel
</Panel>

En algo como esto:

React.createElement(
  Panel,
  {title: 'Opciones', color: 'green'},
  'Texto del panel')

Cada elemento JSX se convierte en una invocación a la función React.createElement, la cual recibe los siguientes parámetros:

  • El tipo del «componente». Puede ser una función constructora si se trata de un componente implementado como una clase; una función que devuelve JSX, si se trata de un stateless functional component, o incluso un string si es un elemento html.
  • Un objeto en el que se agrupan las propiedades (props) que queremos pasarle al componente.
  • El resto de parámetros serán los hijos (children) del componente, que serán accesibles a través de props.children.

Como se puede ver, es todo bastante fácil y la traducción de JSX a invocaciones de funciones Javascript no tiene mucho misterio. Pero esto es sólo la traducción, ¿qué hace la llamada a React.createElement?

Lo más importante para entender cómo funciona React.createElement es ser consciente de que todavía no se está instanciando ningún componente.

En este punto, React.createElement crea una estructura, un Element que contiene la información necesaria para crear el componente. Cuándo y cómo será creado el componente, es algo que se decidirá más adelante.

Esta información incluye los parámetros que hemos indicado desde el JSX junto a alguna información adicional, y además se reempaqueta un poco para darle el aspecto que luego tendremos desde el componente. Por ejemplo, se copian los children, que React.createElement ha recibido por separado, dentro de lo que serán los props del componente una vez creado.

A continuación esta información llegará al subsistema encargado de montar el componente y sincronizar el DOM real con el virtual (ReactMount y ReactReconciler). Estos se encargarán de renderizar el componente, ya sea instanciándolo e invocándo su método render si es un componente de tipo clase, o ejecutándolo si es un componente funcional. Cuando todo haya quedado convertido a etiquetas HTML, se procederá a actualizar el DOM real.

Las implicaciones de este proceso

Todo esto que hemos visto está muy bien como culturilla general y por saber mejor qué terreno pisamos al utilizar ReactJS, pero es que además tiene algunas implicaciones a la hora de desarrollar que te pueden dar alguna sorpresa la primera vez que te las encuentras.

Puede resultar especialmente confuso cuando empiezas a utilizar distintos patrones de reutilización de código entre componentes y tienes que andar pasando componentes, instancias o fragmentos de JSX de un sitio a otro, dependiendo de lo que quieras hacer. Si además no cuentas con la ayuda de algún sistema de tipos (como flow o typescript), es fácil perder la pista de lo que estás haciendo.

Para hacernos una idea de los distintos escenarios que puede haber, vamos a ver de qué formas puedes acabar viendo un componente de tipo clase. Partamos de un componente Panel definido como una clase:

class Panel extends React.Component {
  render() {
    // ... lo que seas
  }
}

Como cualquier otra clase de Javascript, Panel no es más que una función constructora que podemos pasar de un sitio a otro, como veíamos al hablar de componentes de orden superior. Habrá veces que lo que queramos sea tener directamente esa función.

Por ejemplo, podríamos tener una función para encapsular componentes en indistintos tipos de contenedos, y usarla para meter elementos dentro de Panel:

function wrap(Component, children) {
  return <Component>{children}<Component>
}

// Panel (la función constructura) nos serviría en ese caso
wrap(Panel, <p>Hola</p>);
wrap(OtherContainer, <p>Hola</p>);

Otras veces necesitaremos crear «fragmentos de JSX» (en realidad son elementos como los que hemos visto apartado anterior) que incluyan nuestro Panel:

// Aquí content es una definición de cómo crear un
// panel específico, pero NO ES UNA INSTANCIA DE PANEL
let content = <Panel title='Amapola'/>

// Podríamos utilizar ese content ahora como children
// de nuestra función wrap del ejemplo anterior
let tab = wrap(Tab, content);

Es importante ser conscientes de que este caso content no es una instancia de panel. Es un Element que define cómo crear un Panel.

Al ser una clase normal, también podríamos instaciar Panel y obtener un objeto Panel real, aunque no es algo muy útil en este contexto:

// Eso es bastante inútil, pero podrías hacerlo para
// tener una instancia de Panel
let panel = new Panel();

Lo habitual cuando necesitas una instancia de Panel es que sea porque está montado en otro componente, y para eso podrías capturarlo a través de ref:

// Esta es una forma más habitual de tener una instancia
// de panel resultante de montarlo en el DOM dentro del
// método render de otro componente, 
let panel;

<Panel ref={p => panel = p} title='Amapola'/>

// Tras capturar la referencia, panel es una instancia
// de Panel y podríamos acceder a cualquier método que
// estuviera definido en la clase
panel.collapse();

Es importante distinguir los tres escenarios y utilizarlos según necesitemos. A veces necesitaremos tratar con la clase del componente, otras veces estaremos generando la información necesaria para renderizarlo desde JSX, y en ocasiones necesitaremos una referencia al componente ya instanciado y montado en el DOM.

Esto también afecta a la forma en que es tratan los children de un componente.

Si tenemos un componente que actúa como contenedor de otro(s) componente(s), y necesitamos interactuar con sus children, habrá que tener en cuenta que lo que vemos dentro de children no son instancias reales de componentes, son sólo Elements, por lo que no podremos invocar métodos definidos en esos componentes, a menos que obtengamos una referencia a ellos después de ser montados, como veíamos en el ejemplo anterior.

Otro factor a tener en cuenta a la hora de tratar con children es que, dependiendo de lo que nos interese, podemos retrasar más o menos su creación a costa de perder o ganar visibilidad.

Si tuviéramos este componente:

const FunctionalComponent = ({text}) => <p>text</p>;

Podríamos utilizarlo para incluirlo dentro de un Panel:

<Panel><FunctionalComponent text="Rosa"/></Panel>;

Haciéndolo de esta forma, en props.children del Panel habrá un Element para crear nuestro FunctionalComponent, pero el código de FunctionalComponent no se ejecutará hasta que llegue el momento de montarlo en el DOM. Si ese código es lento, con esto estaríamos posponiendo su ejecución hasta que fuera necesario (e incluso evitándola si por algún motivo al final no se renderizase).

Podemos darle la vuelta y hacerlo al revés: ejecutar primero FunctionalComponent e insertar el resultado como props.children de Panel:

 
<Panel>{FunctionalComponent({text: "Rosa"})}</Panel>

Ahora siempre estaremos pagando el precio de ejecutar la función, pero a cambio al renderizar Panel no queda rastro de FunctionalComponent. Esto puede ser útil si Panel necesita retocar sus hijos antes de renderizarlos, por ejemplo para añadirles una clase css o un manejador de eventos a través de sus respectivas props. Si estuviéramos utilizando FunctionalComponent como componente en lugar de invocándolo como una función normal, sería opaco para Panel y no podría modificar los elementos generados por FunctionalComponent al renderizarse.

Utilizar una u otra técnica depende mucho de lo que quieras resolver en cada momento, pero al final en una aplicación no es extraño acabar usando ambas.

Conclusión

Manejar este tipo de situaciones puede ser muy útil y darte un extra de flexibilidad a la hora de diseñar tus aplicaciones con ReactJS. De hecho es una de las cosas que más gusta de ReactJS, la cantidad de opciones que tienes para resolver un problema y los distintos enfoques que le puedes dar a la hora de diseñar la aplicación.

Como suele pasar, con el JSX atraviesas varias fases. Al principio todo te parece fácil porque te limitas a copiar ejemplos, no necesitas hacer cosas raras, y todo fluye. Cuando empiezas a necesitar hacer cosas más complejas e introducir algunas abstracciónes, es inevitable liarse unas cuantas veces entre clases, instancias y descriptores para construir instancias. Una vez que tienes eso interiorizado, le puedes sacar mucho partido y aplicar un patrón u otro dependiendo de lo que necesites en cada momento.

2 comentarios en “Entendiendo cómo funciona JSX

  1. Que buenas explicaciones de las entrañas das, creo que muy poca gente se pregunta como funciona esto y se apalanca tanto en la libreria/framework que se le olvida que es javascript.

Comentarios cerrados.