Emulando React e JSX no Vanilla JS
Publicados: 2022-03-11Poucas pessoas não gostam de frameworks, mas mesmo que você seja um deles, deve tomar nota e adotar os recursos que facilitam um pouco a vida.
Eu era contra o uso de frameworks no passado. Porém, ultimamente, tenho tido a experiência de trabalhar com React e Angular em alguns de meus projetos. As primeiras vezes que abri meu editor de código e comecei a escrever código em Angular, pareceu estranho e antinatural; especialmente depois de mais de dez anos de codificação sem usar nenhum framework. Depois de um tempo, decidi me comprometer a aprender essas tecnologias. Muito rapidamente, uma grande diferença se tornou aparente: era tão fácil manipular o DOM, tão fácil ajustar a ordem dos nós quando necessário, e não eram necessárias páginas e páginas de código para construir uma interface do usuário.
Embora eu ainda prefira a liberdade de não estar ligado a um framework ou arquitetura, não posso ignorar o fato de que criar elementos DOM em um é muito mais conveniente. Então comecei a procurar maneiras de emular a experiência no vanilla JS. Meu objetivo é extrair algumas dessas ideias do React e demonstrar como os mesmos princípios podem ser implementados em JavaScript simples (geralmente chamado de vanilla JS) para tornar a vida do desenvolvedor um pouco mais fácil. Para fazer isso, vamos criar um aplicativo simples para navegar em projetos do GitHub.
Independentemente da forma como estamos construindo um front-end usando JavaScript, estaremos acessando e manipulando o DOM. Para nosso aplicativo, precisaremos construir uma representação de cada repositório (miniatura, nome e descrição) e adicioná-lo ao DOM como um elemento de lista. Usaremos a API de pesquisa do GitHub para buscar nossos resultados. E, já que estamos falando de JavaScript, vamos pesquisar os repositórios de JavaScript. Quando consultamos a API, obtemos a seguinte resposta 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 }, //... ] }
Abordagem do React
O React torna muito simples escrever elementos HTML na página e é um dos recursos que eu sempre quis ter ao escrever componentes em JavaScript puro. O React usa JSX, que é muito semelhante ao HTML normal.
No entanto, não é isso que o navegador lê.
Sob o capô, o React transforma o JSX em um monte de chamadas para uma função React.createElement
. Vamos dar uma olhada em um exemplo de JSX usando um item da API do GitHub e ver o que ele traduz.
<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 é muito simples. Você escreve código HTML regular e injeta dados do objeto adicionando colchetes. O código JavaScript dentro dos colchetes será executado e o valor será inserido no DOM resultante. Uma das vantagens do JSX é que o React cria um DOM virtual (uma representação virtual da página) para rastrear alterações e atualizações. Ao invés de reescrever todo o HTML, o React modifica o DOM da página sempre que a informação é atualizada. Este é um dos principais problemas que o React foi criado para resolver.
abordagem jQuery
Os desenvolvedores costumavam usar muito o jQuery. Eu gostaria de mencioná-lo aqui porque ainda é popular e também porque está bem próximo da solução em JavaScript puro. jQuery obtém uma referência a um nó DOM (ou uma coleção de nós DOM) consultando o DOM. Ele também envolve essa referência com várias funcionalidades para modificar seu conteúdo.
Embora o jQuery tenha suas próprias ferramentas de construção DOM, o que vejo com mais frequência é apenas a concatenação de HTML. Por exemplo, podemos inserir código HTML nos nós selecionados chamando a função html()
. De acordo com a documentação do jQuery, se quisermos alterar o conteúdo de um nó div
com a classe demo-container
podemos fazer assim:
$( "div.demo-container" ).html( "<p>All new content.<em>You bet!</em></p>" );
Essa abordagem facilita a criação de elementos DOM; no entanto, quando precisamos atualizar nós, precisamos consultar os nós de que precisamos ou (mais comumente) voltar a recriar o trecho inteiro sempre que uma atualização for necessária.
Abordagem da API DOM
JavaScript em navegadores tem uma API DOM integrada que nos dá acesso direto para criar, modificar e excluir os nós em uma página. Isso se reflete na abordagem do React e, usando a API DOM, chegamos um passo mais perto dos benefícios dessa abordagem. Estamos modificando apenas os elementos da página que realmente precisam ser alterados. No entanto, o React também acompanha um DOM virtual separado. Ao comparar as diferenças entre o DOM virtual e o real, o React é capaz de identificar quais partes precisam ser modificadas.
Essas etapas extras às vezes são úteis, mas nem sempre, e manipular o DOM diretamente pode ser mais eficiente. Podemos criar novos nós DOM usando a função _document.createElement_
, que retornará uma referência ao nó criado. Manter o controle dessas referências nos dá uma maneira fácil de modificar apenas os nós que contêm a parte que precisa ser atualizada.
Usando a mesma estrutura e fonte de dados do exemplo JSX, podemos construir o DOM da seguinte maneira:
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);
Se a única coisa em sua mente é a eficiência da execução do código, essa abordagem é muito boa. No entanto, a eficiência não é medida apenas na velocidade de execução, mas também na facilidade de manutenção, escalabilidade e plasticidade. O problema com essa abordagem é que ela é muito detalhada e às vezes complicada. Precisamos escrever um monte de chamadas de função mesmo se estivermos apenas construindo uma estrutura básica. A segunda grande desvantagem é o grande número de variáveis criadas e rastreadas. Digamos que um componente que você está trabalhando contenha em seus próprios 30 elementos DOM, você precisará criar e usar 30 elementos e variáveis DOM diferentes. Você pode reutilizar alguns deles e fazer alguns malabarismos às custas da manutenção e plasticidade, mas pode se tornar muito confuso, muito rapidamente.

Outra desvantagem significativa é devido ao número de linhas de código que você precisa escrever. Com o tempo, torna-se cada vez mais difícil mover elementos de um pai para outro. Isso é uma coisa que eu realmente aprecio do React. Posso visualizar a sintaxe JSX e obter em alguns segundos qual nó está contido, onde e alterar, se necessário. E, embora possa parecer que não é grande coisa no começo, a maioria dos projetos tem mudanças constantes que farão você procurar um caminho melhor.
Solução proposta
Trabalhar diretamente com o DOM funciona e faz o trabalho, mas também torna a construção da página muito detalhada, especialmente quando precisamos adicionar atributos HTML e nós aninhados. Então, a ideia seria capturar alguns dos benefícios de trabalhar com tecnologias como JSX e tornar nossa vida mais simples. As vantagens que estamos tentando replicar são as seguintes:
- Escreva código em sintaxe HTML para que a criação de elementos DOM seja fácil de ler e modificar.
- Como não estamos usando um equivalente do DOM virtual como no caso do React, precisamos ter uma maneira fácil de indicar e acompanhar os nós nos quais estamos interessados.
Aqui está uma função simples que faria isso usando um snippet 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; }
A ideia é simples, mas poderosa; enviamos a função o HTML que queremos criar como uma string, na string HTML adicionamos um atributo var aos nós que queremos que as referências sejam criadas para nós. O segundo parâmetro é um objeto no qual essas referências serão armazenadas. Se não for especificado, criaremos uma propriedade “nodes” no nó ou fragmento de documento retornado (caso o nó da hierarquia mais alta seja mais de um). Tudo é feito em menos de 60 linhas de código.
A função funciona em três etapas:
- Crie um novo nó vazio e use innerHTML nesse novo nó para criar toda a estrutura DOM.
- Itere sobre os nós e, se o atributo var existir, adicione uma propriedade no objeto de escopo que aponte para o nó com esse atributo.
- Retorne o nó superior na hierarquia ou um fragmento de documento caso haja mais de um.
Então, como está nosso código para renderizar o exemplo agora?
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;
Primeiro, definimos o objeto (UI) onde armazenaremos as referências aos nós criados. Em seguida, compomos o modelo HTML que usaremos, como uma string, marcando os nós de destino com um atributo “var”. Depois disso, chamamos a função Browser.DOM com o template e o objeto vazio que irá armazenar as referências. Finalmente, usamos as referências armazenadas para colocar os dados dentro dos nós.
Essa abordagem também separa a construção da estrutura DOM e a inserção dos dados em etapas separadas, o que ajuda a manter o código organizado e bem estruturado. Isso nos permite criar separadamente a estrutura DOM e preencher (ou atualizar) os dados quando estiverem disponíveis.
Conclusão
Embora alguns de nós não gostem da ideia de mudar para frameworks e entregar o controle, é importante reconhecer os benefícios que esses frameworks trazem. Há uma razão pela qual eles são tão populares.
E embora um framework nem sempre se adapte ao seu estilo ou necessidades, algumas funcionalidades e técnicas podem ser adotadas, emuladas ou às vezes até desacopladas de um framework. Algumas coisas sempre serão perdidas na tradução, mas muito pode ser ganho e usado por uma pequena fração do custo que um framework carrega.