React y D3.js, trabajando juntos I - Introducción


Esta serie de dos posts viene inspirada por la charla que impartimos en el Open South Code 2018 sobre la integración entre React y D3.js, cuyo material puedes encontrar aqui: fjcalzado.surge.sh. En esta primera parte haremos un breve repaso de ambas librerías, centrándonos principalmente en la gestión del DOM. Veremos conceptos interesantes y concluiremos con la problemática a la hora de trabajar con ambos frameworks. En la segunda parte, explicaremos las distintas soluciones disponibles y las ilustraremos con código y ejemplos. ¡Vamos allá!

react_d3_love.png

Tanto React como D3.js son excelentes librerias, utilizadas extensamente en innumerables proyectos, que nos proporcionan flexibilidad y potencia para acometer desarrollos complejos en el mundo front-end. Es cierto que su propósito difiere en términos generales: mientras que React está enfocado a la creación de interfaces de usuario basados en componentes, D3.js se especializa en la visualización interactiva de datos. No obstante, ambos frameworks en esencia, nos ayudan en la engorrosa tarea de gestionar eficientemente el DOM, y se sirven de mecanismos parecidos para conseguirlo

React

La popularidad de React es bien merecida: nos permite construir interfaces de usuario muy eficientes y fácilmente depurables mediante la composición de componentes reusables. No vamos a entrar en las tripas de esta librería pero si que nos pararemos a entender una de las claves de su éxito: el DOM virtual.

Cada componente de React monitoriza su estado y propiedades para poder renderizarse a si mismo si fuese necesario. Además, lo hará de forma eficiente, contribuyendo al rendimiento de la aplicación. Un cambio en el estado o propiedades de un componente disparará una actualización del DOM. Sin embargo, ésta no se aplicará sobre el DOM real sino sobre una representación del mismo en memoria, denominada DOM virtual. Ten en cuenta que una actualización del DOM puede ser una operación costosa mientras que un cambio en memoria lo será mucho menos.

De este modo, los cambios de cada uno de los componentes van a parar a memoria, al DOM virtual. Puedes visualizar este DOM virtual como si de un buffer o borrador se tratara. Es una potente herramienta que actúa como capa intermedia recolectando cambios con un mínimo impacto en el rendimiento. Posteriormente, en un proceso llamado reconciliación, un algoritmo diff compara el estado del DOM virtual con una versión anterior del mismo para extraer de este modo la delta (o diferencia). Esta delta no es otra cosa que el conjunto de cambios realmente significativos que deberán ser finalmente aplicados sobre el DOM real. Este proceso mantiene sincronizados el DOM virtual con el real empleando para ello el mínimo número de operaciones posible y, por tanto, el rendimiento se optimiza en gran medida.

dom_virtual.png

En definitiva, React nos abstrae de realizar cambios manuales, manipulaciones o manejo de eventos del DOM real. Es el DOM virtual quien recibe todos estos cambios, pero actúa de parapeto o filtro, evitando que todos los cambios disparados por los componentes promocionen inmediatamente al DOM real. En lugar de eso los reconcilia, y una vez calculado el siguiente estado con los mínimos cambios necesarios, los propaga de manera efectiva y eficiente al DOM real en un proceso transparente a nosotros.

D3.js

D3.js es una librería para la creación y manipulación de interfaces que representan datos, o dicho de otro modo, para la visualización interactiva de datos. De hecho, su siglas lo ponen de manifiesto: Data-Driven Documents. Nos provee de las herramientas necesarias para crear visualizaciones o gráficos, basados principalmente en SVG y con énfasis en la representacion en tiempo real, transiciones, animaciones, etc.

La magia de D3.js, y parte del secreto de su éxito, recae en el concepto de las uniones o data joins. Con D3.js establecemos literalmente un vínculo entre los datos que queremos representar y los elementos que se usarán para representarlos. Para entender esto, hagamos un ejercicio. Imagina que tenemos un listado con datos sobre paises, como por ejemplo el número de habitantes y otros datos de interés:

countries_table.png

Hagamos una representación mental de estos datos. ¿Lo intentas? Imagina un lienzo en blanco, donde cada país se representa como un círculo. Además, el tamaño de dicho círculo será proporcional a la población: un país pequeño será representado como un círculo pequeño, un circulo grande indicará un país muy poblado. El color del círculo dependerá del continente al que pertenezca el país. Finalmente, la posición horizontal de cada círculo en el gráfico vendrá determinada por su poder adquisitivo (los países más ricos a la derecha, los más pobres a la izquierda) mientras que la posición vertical dependerá de su esperanza de vida (arriba los más longevos, abajo los menos afortunados). El resultado sería algo parecido a esto:

countries_graph.png

Si has seguido el ejercicio, habrás tenido que hacer una asociación mental gracias a la cual, cada fila de la tabla (cada país) era convertido en un círculo. Además, las propiedades de ese círculo quedaban vinculadas directamente a los datos de la tabla. Esto es exactamente lo que hace D3.js por ti. Establece un enlace entre cada fila de la tabla (denominada datum en la jerga D3) y el nodo SVG que lo representa (llamado elemento).

Selecciones

La selección es el mecanismo por el que se registran y establecen las uniones (o data joins) entre datos y elementos. Mediante una selección, indicamos a D3.js con que datos queremos vincular los elementos seleccionados. De este modo, D3.js siempre tiene acceso a los datos que generaron cada uno de los elementos que vemos en nuestro gráfico. Ante un hipotético cambio en estos datos (datos en tiempo real), D3.js comparará los nuevos datos con los ya vinculados y de este modo puede decidir cómo actuar según 3 posibles escenarios:

  • Datos para los que todavía no tenemos elemento asociado. Habrá que crear un elemento nuevo y vincularlo. A este grupo se le conoce como enter.

  • Datos que se actualizan y provocan que el elemento asociado también deba ser actualizado. A este grupo se le llama update.

  • Datos que desaparecen, no los recibimos más. Por tanto su elemento vinculado deberá ser eliminado (en la mayoría de los casos es lo deseable aunque no tiene por que ser asi siempre). Grupo llamado exit.

En nuestro ejemplo anterior, necesitamos asociar cada nueva fila de la tabla (que representa un país) a un círculo. Por tanto, aplicaremos una selección sobre el grupo enter para crear un círculo por cada datum de entrada y aplicarle un color, un tamaño y una posición.

const paises = [...];
const circulos = svg.selectAll("circle")
  .data(paises)
  .enter()
  .append("circle")
    .attr("cx", pais => xScale(pais.poderAdquisitivo))
    .attr("cy", pais => yScale(pais.esperanzaVida))
    .attr("r", pais => areaScale(pais.habitantes))
    .attr("fill", pais => color(pais.continente));

D3.js mantendrá viva esta correspondencia entre elementos y datos y si prevemos que los datos puedan cambiar, podemos hacer que los círculos asociados se actualicen (implementando el patrón update) o eliminar aquellos que queden huérfanos (mediante el patrón exit).

Es decir, podemos visualizar datos en tiempo real, haciendo que los elementos del gráfico reaccionen ante cualquier modificación en los datos. O dicho al revés, manteniendo el UI sincronizado con los datos. ¿Esto nos suena verdad? Por supuesto, ¡React hace lo mismo! Y es que, en la práctica, las uniones de datos en D3.js sirven como algoritmo diff entre los nuevos datos y los anteriores y permiten actuar sobre el DOM mutándolo con cierto grado de eficiencia de acuerdo a los patrones enterupdate y exit.

Pugna por el DOM

Como hemos visto en los apartados anteriores, tanto React como D3.js comparten una filosofía común: "si tu me proporcionas el estado, yo mantendré el DOM actualizado por ti". React lo hará mediante el DOM virtual y la reconciliación. D3.js gracias a los vínculos entre datos y elementos. Ambos, en el fondo, toman el control del DOM real con el propósito de ayudar al desarrollador restando complejidad.

Pero, ¿y si combinamos las dos librerias? ¿qué sucederá? Es evidente que entraremos en conflicto, habrá una pugna por el DOM. En el caso concreto de React, todos los cambios deben seguir su mecanismo habitual por el que se aplican al DOM virtual y posteriormente se reconcilian. De no ser así, suponiendo por ejemplo una mutación por parte de D3.js sobre un nodo controlado por React, acabaríamos corrompiendo la sincronización que React mantiene entre su DOM virtual y real. Los resultados son imprevisibles, pero muy probablemente no deseados. Como regla general, debemos evitar que React y D3.js compartan el control del DOM.

Continuación (implementación) ...