Resolvendo um exercício de um curso e indo um pouco além...

Saudações, colegas. Comecei a fazer um curso muito bom de React, o Joy of React, do Joshua Comeau. Logo no começo do primeiro módulo, após um "Hello World" com React, é apresentado um exercício entitulado "Construa seu próprio React" (Bem exagerado, rs, até porque deveria se chamar "Construa seu próprio ReactDOM"). Nele, devemos criar uma função "render", como a do react-dom, que vai receber e renderizar um objeto similar a um elemento React real, retornado por React.createElement():

function render(reactElement, containerDOMElement) {
  /* Seu código aqui! */
}

const reactElement = {
  type: 'a',
  props: {
    href: 'https://wikipedia.org/',
  },
  children: 'Read more on Wikipedia',
};

const containerDOMElement =
  document.querySelector('#root');

render(reactElement, containerDOMElement);

Os critérios de aceitação são:

  • Um link deve ser exibido no painel "Resultado", linkando a wikipedia.org, e com o texto “Read more on Wikipedia”.
  • Deve funcionar com qualquer tipo de elemento (ex. âncoras, parágrafos, botões…).
  • Deve lidar com todos os atributos HTML (ex. href, id, disabled…).
  • O elemento deve conter o texto especificado em children. children será sempre uma string.

Após um pequena revisão da parte mais trivial da api do DOM, cheguei em uma solução praticamente igual à solução proposta:

function render(reactElement, containerDOMElement) {
  // 1. cria um elemento DOM
  const domElement = document.createElement(reactElement.type);

  // 2. seta as propriedades
  domElement.innerText = reactElement.children;
  for (const key in reactElement.props) {
    const value = reactElement.props[key];
    domElement.setAttribute(key, value);
  }

  // 3. coloca no container
  containerDOMElement.appendChild(domElement);
}

Neste ponto pensei: "Blz. Bola pra frente. Bora aprender react!". Mas logo em seguida me bateu a curiosidade: "Cara, com o pouquinho de javascript que aprendi, será se consigo implementar algo um pouco mais próximo dos casos de uso reais?". Por exemplo, será se dava pra expandir essa função para renderizar uma árvore de elementos, em vez de 1 elemento apenas? Além disso, e se esses elementos aninhados também fossem funções que retornam elementos, como os componentes React?

Então resolvi tentar e cheguei na solução a seguir. Coloquei em um arquivo html único e comentei o código:

<!DOCTYPE html>
<html>

<body>
  <div id="root"></div>
</body>
<script>

  function render(reactElement, containerDOMElement) {

    // Desestrutura reactElement, pra poupar ter que digitar reactElement.<attr> toda vez.
    // A atribuição padrão de {} é para não dar erro na desestruturação,
    // em caso de null ou undefined, e sempre lançar um Error('Not a valid element!').
    const { type, props, children } = reactElement || {};

    // Se o objeto passado tem um atributo type que é o nome de uma "tag":
    if (typeof type === 'string') {

      // Criamos um elemento DOM do tipo definido pela tag;
      const DOMElement = document.createElement(type);
      // Adicionamos atributos no elemento correspondentes a cada prop;
      for (const key in props) DOMElement.setAttribute(key, props[key]);

      // Normaliza qualquer valor de children, mesmo inexistente, para ser um array;
      const childrenList = [].concat(children);
      // Itera no array de filhos, renderiza cada filho e adiciona ao elemento que criamos.
      childrenList.forEach( child => renderChild(child, DOMElement) );
      
      // Se o container passado for um elemento DOM,
      if (
        containerDOMElement instanceof Element ||
        containerDOMElement instanceof HTMLDocument
      ) {
        // Adiciona a ele o elemento que criamos.
        containerDOMElement.appendChild(DOMElement);
      } else {
        // Se não for um elemento DOM, lança um erro.
        throw new Error('Not a DOM element!');
      }
    // Se o objeto passado tem um atributo type que é o nome de uma função:
    } else if (typeof type === 'function') {
      // Renderizamos o retorno da invocação da função e o adicionamos ao container.
      render(type.call(type, props, children), containerDOMElement);
    } else { // Se não é um objeto que possui um type do tipo "tag string" ou função,
      // lança uma erro.
      throw new Error('Not a valid element!');
    }
  }

  // Essa sub-rotina poderia estar do corpo da função render, mas preferi separar,
  // para melhorar a legibilidade e navegabilidade do código. Dá pra separar mais
  // código da função principal, para que o código seja mais legível e auto-documentado,
  // mas não estou tããão na vibe "clean code", já que não é código de aplicação real.
  // A função renderiza um filho(elemento ou valor primitivo) e adiciona ao pai.
  function renderChild(child, parentDOMElement) {
    // Se for um objeto e um elemento "React" válido em potencial,
    if (typeof child === 'object' && child !== null) {
      // chamamos render recursivamente.
      // Note que arrays são objetos e por isso serão passados para a função render.
      // Como não queremos renderizar filhos do tipo array, deixamos que caia no Error('Not a valid element!').
      render(child, parentDOMElement);
    } else { // Se não for um objeto,
      // adicionamos o texto do "filho" ao conteúdo do elemento pai, caso o filho não seja null ou undefined.
      // Anteriormente, eu só testava se era falsy, mas analogamente ao desenvolvimento real,
      // provavelmente queremos que um valor como 0 ou NaN seja renderizado no corpo da página. 
      // A conversão String(child) só é necessária pois, ao usar a coerção da concatenação com string,
      // dá erro quando se usa symbol. Preciosismo, já que é só um exercício. 
      (child !== undefined && child !== null) && (parentDOMElement.innerHTML += String(child));
    }
  };

  // Declaramos uma função pra representar um "componente React", que retorna um objeto que representa
  // um elemento HTML do tipo âncora;
  function Link(props = {}, children = 'Provide a link text') {
    const { href = '#', ...rest } = props;
    return {
      type: 'a',
      props: {
        href: href,
        ...rest
      },
      children: children
    };
  }

  // Criamos um elemento "funcional" do tipo declarado acima;
  const reactLinkElement = {
    type: Link,
    props: {
      href: 'https://wikipedia.org/',
    },
    children: 'Read more on Wikipedia.',
  };

  // Criamos um elemento HTML p e colocamos como filhos um texto e o elemento acima;
  const pElement = {
    type: 'p',
    props: { id: 'par' },
    children: ['Leia mais na wikipedia: ', reactLinkElement, '']
  };

  // Declaramos mais um componente, só pra ter mais coisa pra renderizar na nossa "App";
  function Header() {
    return {
      type: 'header',
      children: [
        {
          type: 'h1',
          children: 'Titulo da App'
        },
        pElement
      ]
    };
  }

  // Criamos um elemento "funcional" do tipo acima;
  const headerElement = {
    type: Header
  };

  // Declaramos nosso componente App, que conterá tudo o que criamos acima, além de valores filho null, undefined, 0 e NaN.
  function App() {
    return {
      type: 'div',
      props: { id: 'app' },
      children: [
        undefined,
        headerElement,
        'Esta app é muito criativa e útil: ',
        null,
        reactLinkElement,
        ' outra vez...',
        0,
        ' ',
        NaN
      ]
    };
  }

  // Pegamos um referência ao elemento da página que conterá a App...
  const containerDOMElement = document.querySelector('#root');

  // .. e renderizamos um "elemento" tipo "App" nesse container. Ufa!
  render({ type: App }, containerDOMElement);

</script>

</html>

Se quiser testar esse código, basta salvar num arquivo .html e abrir no navegador. O resultado será semelhante ao seguinte:

Titulo da App

Leia mais na wikipedia: Read more on Wikipedia.

Esta app é muito criativa e útil: Read more on Wikipedia. outra vez...0 NaN

Eu gostaria de saber se mais alguém gosta de estudar explorando idéias dessa forma. Pessoalmente, me ajuda a consolidar o que aprendi ao analisar os novos conceitos de forma exploratória. Assim como os Code Playgrounds ajudam a testar código de forma rápida, ao criar esses "playgrounds" de aprendizado e experimentação, consigo aprender qualquer novo assunto de forma interessante e fluida. E, por favor, me digam se acham que algo nesse código poderia melhorar e sob qual ponto de vista. Obrigado!

Por fim, quero recomendar um artigo muito bom sobre aprendizado, escrito pelo criador do curso que estou fazendo:

How To Learn Stuff Quickly, por Joshua Comeau.