Emular React y JSX en Vanilla JS
Publicado: 2022-03-11A pocas personas les disgustan los marcos, pero incluso si usted es uno de ellos, debe tomar nota y adoptar las funciones que hacen la vida un poco más fácil.
Me opuse al uso de marcos en el pasado. Sin embargo, últimamente he tenido la experiencia de trabajar con React y Angular en algunos de mis proyectos. Las primeras veces que abrí mi editor de código y comencé a escribir código en Angular me pareció extraño y poco natural; especialmente después de más de diez años de codificación sin usar ningún marco. Después de un tiempo, decidí comprometerme a aprender estas tecnologías. Rápidamente se hizo evidente una gran diferencia: era muy fácil manipular el DOM, era muy fácil ajustar el orden de los nodos cuando era necesario, y no se necesitaron páginas y páginas de código para crear una interfaz de usuario.
Aunque todavía prefiero la libertad de no estar apegado a un marco o una arquitectura, no podía ignorar el hecho de que crear elementos DOM en uno es mucho más conveniente. Así que comencé a buscar formas de emular la experiencia en Vanilla JS. Mi objetivo es extraer algunas de esas ideas de React y demostrar cómo se pueden implementar los mismos principios en JavaScript simple (a menudo denominado JS estándar) para facilitar un poco la vida de los desarrolladores. Para lograr esto, construyamos una aplicación simple para explorar proyectos de GitHub.
Cualquiera que sea la forma en que construyamos una interfaz usando JavaScript, accederemos y manipularemos el DOM. Para nuestra aplicación, necesitaremos construir una representación de cada repositorio (miniatura, nombre y descripción) y agregarlo al DOM como un elemento de lista. Usaremos la API de búsqueda de GitHub para obtener nuestros resultados. Y, ya que estamos hablando de JavaScript, busquemos en los repositorios de JavaScript. Cuando consultamos la API, obtenemos la siguiente respuesta JSON:
{ "total_count": 398819, "incomplete_results": false, "items": [ { "id": 28457823, "name": "freeCodeCamp", "full_name": "freeCodeCamp/freeCodeCamp", "owner": { "login": "freeCodeCamp", "id": 9892522, "avatar_url": "https://avatars0.githubusercontent.com/u/9892522?v=4", "gravatar_id": "", "url": "https://api.github.com/users/freeCodeCamp", "site_admin": false }, "private": false, "html_url": "https://github.com/freeCodeCamp/freeCodeCamp", "description": "The https://freeCodeCamp.org open source codebase "+ "and curriculum. Learn to code and help nonprofits.", // more omitted information }, //... ] }
El enfoque de React
React simplifica mucho la escritura de elementos HTML en la página y es una de las características que siempre quise tener al escribir componentes en JavaScript puro. React usa JSX, que es muy similar al HTML normal.
Sin embargo, eso no es lo que lee el navegador.
Bajo el capó, React transforma JSX en un montón de llamadas a una función React.createElement
. Echemos un vistazo a un ejemplo de JSX usando un elemento de la API de GitHub y veamos a qué se traduce.
<div className="repository"> <div>{item.name}</div> <p>{item.description}</p> <img src={item.owner.avatar_url} /> </div>;
; React.createElement( "div", { className: "repository" }, React.createElement( "div", null, item.name ), React.createElement( "p", null, item.description ), React.createElement( "img", { src: item.owner.avatar_url } ) );
JSX es muy simple. Usted escribe código HTML regular e inyecta datos del objeto agregando corchetes. Se ejecutará el código JavaScript entre corchetes y el valor se insertará en el DOM resultante. Una de las ventajas de JSX es que React crea un DOM virtual (una representación virtual de la página) para rastrear cambios y actualizaciones. En lugar de reescribir todo el HTML, React modifica el DOM de la página cada vez que se actualiza la información. Este es uno de los principales problemas para los que se creó React.
enfoque jQuery
Los desarrolladores solían usar mucho jQuery. Me gustaría mencionarlo aquí porque todavía es popular y también porque es bastante parecido a la solución en JavaScript puro. jQuery obtiene una referencia a un nodo DOM (o una colección de nodos DOM) consultando el DOM. También envuelve esa referencia con varias funcionalidades para modificar su contenido.
Si bien jQuery tiene sus propias herramientas de construcción de DOM, lo que veo con mayor frecuencia en la naturaleza es solo la concatenación de HTML. Por ejemplo, podemos insertar código HTML en los nodos seleccionados llamando a la función html()
. Según la documentación de jQuery, si queremos cambiar el contenido de un nodo div
con la clase demo-container
podemos hacerlo así:
$( "div.demo-container" ).html( "<p>All new content.<em>You bet!</em></p>" );
Este enfoque facilita la creación de elementos DOM; sin embargo, cuando necesitamos actualizar los nodos, debemos consultar los nodos que necesitamos o (más comúnmente) volver a crear el fragmento completo siempre que se requiera una actualización.
Enfoque de la API DOM
JavaScript en los navegadores tiene una API DOM incorporada que nos brinda acceso directo para crear, modificar y eliminar los nodos en una página. Esto se refleja en el enfoque de React y, al usar la API DOM, nos acercamos un paso más a los beneficios de ese enfoque. Estamos modificando solo los elementos de la página que realmente requieren ser cambiados. Sin embargo, React también realiza un seguimiento de un DOM virtual separado. Al comparar las diferencias entre el DOM virtual y el real, React puede identificar qué partes requieren modificación.
Esos pasos adicionales a veces son útiles, pero no siempre, y manipular el DOM directamente puede ser más eficiente. Podemos crear nuevos nodos DOM usando la función _document.createElement_
, que devolverá una referencia al nodo creado. Hacer un seguimiento de estas referencias nos brinda una manera fácil de modificar solo los nodos que contienen la parte que necesita ser actualizada.
Usando la misma estructura y fuente de datos que en el ejemplo de JSX, podemos construir el DOM de la siguiente manera:
var item = document.createElement('div'); item.className = 'repository'; var nameNode = document.createElement('div'); nameNode.innerHTML = item.name item.appendChild(nameNode); var description = document.createElement('p'); description.innerHTML = item.description; item.appendChild(description ); var image = new Image(); Image.src = item.owner.avatar_url; item.appendChild(image); document.body.appendChild(item);
Si lo único que tiene en mente es la eficiencia de la ejecución del código, entonces este enfoque es muy bueno. Sin embargo, la eficiencia no se mide simplemente en la velocidad de ejecución, sino también en la facilidad de mantenimiento, escalabilidad y plasticidad. El problema con este enfoque es que es muy detallado y, a veces, complicado. Necesitamos escribir un montón de llamadas a funciones incluso si solo estamos construyendo una estructura básica. La segunda gran desventaja es la gran cantidad de variables creadas y rastreadas. Digamos que un componente en el que está trabajando contiene en sí mismo 30 elementos DOM, deberá crear y usar 30 elementos y variables DOM diferentes. Puede reutilizar algunos de ellos y hacer algunos malabarismos a expensas de la facilidad de mantenimiento y la plasticidad, pero puede volverse muy complicado, muy rápidamente.

Otra desventaja significativa se debe a la cantidad de líneas de código que necesita escribir. Con el tiempo, se vuelve cada vez más difícil mover elementos de un padre a otro. Eso es algo que realmente aprecio de React. Puedo ver la sintaxis JSX y obtener en unos segundos qué nodo está contenido, dónde y cambiarlo si es necesario. Y, si bien puede parecer que no es gran cosa al principio, la mayoría de los proyectos tienen cambios constantes que lo harán buscar una mejor manera.
Solución propuesta
Trabajar directamente con el DOM funciona y hace el trabajo, pero también hace que la construcción de la página sea muy detallada, especialmente cuando necesitamos agregar atributos HTML y anidar nodos. Entonces, la idea sería capturar algunos de los beneficios de trabajar con tecnologías como JSX y simplificar nuestras vidas. Las ventajas que estamos tratando de replicar son las siguientes:
- Escriba código en sintaxis HTML para que la creación de elementos DOM sea fácil de leer y modificar.
- Dado que no estamos usando un equivalente de DOM virtual como en el caso de React, debemos tener una manera fácil de indicar y realizar un seguimiento de los nodos que nos interesan.
Aquí hay una función simple que lograría esto usando un fragmento de código HTML.
Browser.DOM = function (html, scope) { // Creates empty node and injects html string using .innerHTML // in case the variable isn't a string we assume is already a node var node; if (html.constructor === String) { var node = document.createElement('div'); node.innerHTML = html; } else { node = html; } // Creates of uses and object to which we will create variables // that will point to the created nodes var _scope = scope || {}; // Recursive function that will read every node and when a node // contains the var attribute add a reference in the scope object function toScope(node, scope) { var children = node.children; for (var iChild = 0; iChild < children.length; iChild++) { if (children[iChild].getAttribute('var')) { var names = children[iChild].getAttribute('var').split('.'); var obj = scope; while (names.length > 0) { var _property = names.shift(); if (names.length == 0) { obj[_property] = children[iChild]; } else { if (!obj.hasOwnProperty(_property)){ obj[_property] = {}; } obj = obj[_property]; } } } toScope(children[iChild], scope); } } toScope(node, _scope); if (html.constructor != String) { return html; } // If the node in the highest hierarchy is one return it if (node.childNodes.length == 1) { // if a scope to add node variables is not set // attach the object we created into the highest hierarchy node // by adding the nodes property. if (!scope) { node.childNodes[0].nodes = _scope; } return node.childNodes[0]; } // if the node in highest hierarchy is more than one return a fragment var fragment = document.createDocumentFragment(); var children = node.childNodes; // add notes into DocumentFragment while (children.length > 0) { if (fragment.append){ fragment.append(children[0]); }else{ fragment.appendChild(children[0]); } } fragment.nodes = _scope; return fragment; }
La idea es simple pero poderosa; enviamos a la función el HTML que queremos crear como una cadena, en la cadena HTML agregamos un atributo var a los nodos que queremos que tengan referencias creadas para nosotros. El segundo parámetro es un objeto en el que se almacenarán esas referencias. Si no se especifica, crearemos una propiedad de "nodos" en el nodo o fragmento de documento devuelto (en caso de que el nodo de mayor jerarquía sea más de uno). Todo se logra en menos de 60 líneas de código.
La función funciona en tres pasos:
- Cree un nuevo nodo vacío y use innerHTML en ese nuevo nodo para crear la estructura DOM completa.
- Repita los nodos y, si existe el atributo var, agregue una propiedad en el objeto de alcance que apunte al nodo con ese atributo.
- Devuelve el nodo superior de la jerarquía o un fragmento de documento en caso de que haya más de uno.
Entonces, ¿cómo se ve ahora nuestro código para representar el ejemplo?
var UI = {}; var template = ''; template += '<div class="repository">' template += ' <div var="name"></div>'; template += ' <p var="text"></p>' template += ' <img var="image"/>' template += '</div>'; var item = Browser.DOM(template, UI); UI.name.innerHTML = data.name; UI.text.innerHTML = data.description; UI.image.src = data.owner.avatar_url;
Primero, definimos el objeto (UI) donde almacenaremos las referencias a los nodos creados. Luego, creamos la plantilla HTML que vamos a usar, como una cadena, marcando los nodos de destino con un atributo "var". Después de eso, llamamos a la función Browser.DOM con la plantilla y el objeto vacío que almacenará las referencias. Finalmente, usamos las referencias almacenadas para colocar los datos dentro de los nodos.
Este enfoque también separa la construcción de la estructura DOM y la inserción de los datos en pasos separados, lo que ayuda a mantener el código organizado y bien estructurado. Esto nos permite crear por separado la estructura DOM y completar (o actualizar) los datos cuando estén disponibles.
Conclusión
Si bien a algunos de nosotros no nos gusta la idea de cambiar a marcos y entregar el control, es importante que reconozcamos los beneficios que brindan esos marcos. Hay una razón por la que son tan populares.
Y aunque es posible que un marco no siempre se adapte a su estilo o necesidades, algunas funcionalidades y técnicas se pueden adoptar, emular o, a veces, incluso desacoplar de un marco. Algunas cosas siempre se perderán en la traducción, pero se puede ganar y usar mucho a una pequeña fracción del costo que conlleva un marco.