Implementando um Mini React do Zero: createElement, render e Hooks: Do Básico ao Avançado Já leu

Entendendo a Arquitetura do React React é uma biblioteca que revolucionou a forma como construímos interfaces. Sua beleza reside na simplicidade do modelo mental: componentes são funções que retornam descrições de UI, e React gerencia a atualização eficiente do DOM. Para compreender verdadeiramente como React funciona, vamos construir uma versão simplificada que captura os conceitos essenciais. A arquitetura do React se baseia em três pilares fundamentais: a representação declarativa da UI através de elementos (criados com ), a transformação dessa representação em DOM real (através de ), e a capacidade de componentes manterem estado e reagirem a mudanças (através de hooks). Entender esses pilares é crucial porque quando você domina os mecanismos internos, escreve código React mais eficaz e consegue debugar problemas muito mais rapidamente. O que é um Elemento React? Um elemento React não é um componente — é a representação mais básica de algo que você quer renderizar. Pense em um elemento como um objeto JavaScript comum que

Entendendo a Arquitetura do React

React é uma biblioteca que revolucionou a forma como construímos interfaces. Sua beleza reside na simplicidade do modelo mental: componentes são funções que retornam descrições de UI, e React gerencia a atualização eficiente do DOM. Para compreender verdadeiramente como React funciona, vamos construir uma versão simplificada que captura os conceitos essenciais.

A arquitetura do React se baseia em três pilares fundamentais: a representação declarativa da UI através de elementos (criados com createElement), a transformação dessa representação em DOM real (através de render), e a capacidade de componentes manterem estado e reagirem a mudanças (através de hooks). Entender esses pilares é crucial porque quando você domina os mecanismos internos, escreve código React mais eficaz e consegue debugar problemas muito mais rapidamente.

O que é um Elemento React?

Um elemento React não é um componente — é a representação mais básica de algo que você quer renderizar. Pense em um elemento como um objeto JavaScript comum que descreve: "Eu quero um botão, com essas props, com esses filhos". Isso é tudo. Não há magia aqui. Um elemento é apenas dados, uma estrutura que diz ao React o que renderizar.

// Um elemento é simplesmente um objeto descritivo
const elemento = {
  type: 'button',
  props: {
    className: 'btn-primary',
    onClick: () => alert('Clicado!'),
    children: ['Clique aqui']
  }
};

Implementando createElement

createElement é a função que você chama (ou que o JSX transpila para chamar) para criar esses objetos que descrevem a UI. Quando você escreve <Button />, o transpilador converte isso em createElement(Button, null). É um passo crucial compreender que JSX é apenas açúcar sintático.

A Função createElement

A função createElement recebe três argumentos principais: o tipo (pode ser uma string como 'div' ou uma função de componente), as props (propriedades e atributos), e os filhos (children). Ela retorna um objeto simples que descreve o elemento.

function createElement(type, props, ...children) {
  // Se não há props, inicializa um objeto vazio
  const elementProps = props || {};

  // Os children precisam ser normalizados: arrays aninhados viram um array único
  const flatChildren = children.flat();

  // Se há filhos, eles são adicionados às props
  if (flatChildren.length > 0) {
    elementProps.children = flatChildren.length === 1 
      ? flatChildren[0] 
      : flatChildren;
  }

  // Retorna o objeto que descreve o elemento
  return {
    type,
    props: elementProps
  };
}

// Exemplos de uso:
const elemento1 = createElement('div', { className: 'container' }, 
  createElement('h1', null, 'Olá Mundo')
);

const elemento2 = createElement('button', { onClick: () => {} }, 'Enviar');

console.log(elemento1);
// Output: {
//   type: 'div',
//   props: {
//     className: 'container',
//     children: { type: 'h1', props: { children: 'Olá Mundo' } }
//   }
// }

Lidando com Componentes Funcionais

Componentes funcionais são diferentes de elementos primitivos. Um componente é uma função que retorna um elemento. Quando createElement recebe uma função (ao invés de uma string), ele não deve criar um objeto imediatamente — esse trabalho será feito durante a renderização.

function Botao({ label, onClick }) {
  return createElement('button', { onClick, className: 'btn' }, label);
}

// Quando chamamos createElement com um componente:
const meuBotao = createElement(Botao, { label: 'Clique', onClick: () => {} });

console.log(meuBotao);
// Output: {
//   type: Botao,  // A função em si, não o resultado
//   props: { label: 'Clique', onClick: ... }
// }

Implementando Render

render é onde a mágica acontece: ele pega esses objetos descritivos (elementos) e os transforma em DOM real, inserindo-os na página. Este é também o ponto onde precisamos lidar com atualizações eficientes — React não reconstrói tudo do zero, ele difere o novo estado do antigo e aplica apenas as mudanças necessárias.

A Função Render Básica

Começamos com uma implementação simples que apenas cria o DOM. Depois evoluiremos para lidar com atualizações e reconciliação.

function render(elemento, container) {
  // Se o elemento é texto ou número, cria um nó de texto
  if (typeof elemento === 'string' || typeof elemento === 'number') {
    container.appendChild(document.createTextNode(elemento));
    return;
  }

  // Se é um array, renderiza cada item
  if (Array.isArray(elemento)) {
    elemento.forEach(el => render(el, container));
    return;
  }

  // Se é null ou undefined, não faz nada
  if (!elemento) {
    return;
  }

  const { type, props } = elemento;

  // Se type é uma string, é um elemento HTML primitivo
  if (typeof type === 'string') {
    // Cria o elemento DOM
    const domElement = document.createElement(type);

    // Aplica as props (atributos, listeners, etc)
    Object.entries(props).forEach(([key, value]) => {
      if (key === 'children') {
        // Children precisam ser renderizados recursivamente
        if (value) {
          render(value, domElement);
        }
      } else if (key.startsWith('on')) {
        // Listeners de eventos (onClick, onChange, etc)
        const eventName = key.substring(2).toLowerCase();
        domElement.addEventListener(eventName, value);
      } else if (key !== 'key') {
        // Atributos normais
        domElement.setAttribute(key, value);
      }
    });

    container.appendChild(domElement);
  } else if (typeof type === 'function') {
    // Se type é uma função, é um componente
    // Chama a função com as props para obter o elemento que ela retorna
    const componentElement = type(props);
    // Renderiza o elemento retornado
    render(componentElement, container);
  }
}

// Exemplo de uso:
const app = createElement('div', { className: 'app' },
  createElement('h1', null, 'Meu Mini React'),
  createElement('p', null, 'Isso é incrível!')
);

render(app, document.getElementById('root'));

Renderização com Componentes e Estado

A coisa fica mais interessante quando queremos que componentes mantenham estado. Para isso, precisamos rastrear qual componente está sendo renderizado no momento. Esta é a base dos hooks do React.

// Rastreadores globais para hooks
let currentComponent = null;
let componentHooks = new Map();
let hookIndex = 0;

function render(elemento, container) {
  if (typeof elemento === 'string' || typeof elemento === 'number') {
    container.appendChild(document.createTextNode(elemento));
    return;
  }

  if (Array.isArray(elemento)) {
    elemento.forEach(el => render(el, container));
    return;
  }

  if (!elemento) {
    return;
  }

  const { type, props } = elemento;

  if (typeof type === 'string') {
    const domElement = document.createElement(type);

    Object.entries(props).forEach(([key, value]) => {
      if (key === 'children') {
        if (value) {
          render(value, domElement);
        }
      } else if (key.startsWith('on')) {
        const eventName = key.substring(2).toLowerCase();
        domElement.addEventListener(eventName, value);
      } else if (key !== 'key') {
        domElement.setAttribute(key, value);
      }
    });

    container.appendChild(domElement);
  } else if (typeof type === 'function') {
    // Antes de chamar o componente, definimos qual é o componente atual
    currentComponent = type;
    hookIndex = 0;

    // Se não há hooks registrados para este componente, cria um array vazio
    if (!componentHooks.has(type)) {
      componentHooks.set(type, []);
    }

    const componentElement = type(props);
    render(componentElement, container);
  }
}

Implementando Hooks (useState e useEffect)

Hooks são funções que permitem componentes funcionais acessarem recursos que antes eram exclusivos de componentes de classe, como estado e efeitos colaterais. useState é o hook mais fundamental — ele permite que um componente tenha estado. useEffect permite executar código em resposta a mudanças.

useState: Adicionando Estado a Componentes Funcionais

useState retorna um array com dois elementos: o valor atual do estado e uma função para atualizá-lo. Cada componente possui seu próprio conjunto de estados, e precisamos rastreá-los por índice (é por isso que a ordem dos hooks importa).

function useState(initialValue) {
  // Obtém os hooks do componente atual
  const hooks = componentHooks.get(currentComponent);

  // O índice do hook atual (useState, outro useState, useEffect, etc)
  const index = hookIndex;
  hookIndex++;

  // Se este hook não foi inicializado ainda, inicializa
  if (!hooks[index]) {
    hooks[index] = {
      value: typeof initialValue === 'function' 
        ? initialValue() 
        : initialValue,
      setState: null // Será definido abaixo
    };
  }

  const hook = hooks[index];

  // Define a função setState que atualiza o estado
  hook.setState = (newValue) => {
    const actualNewValue = typeof newValue === 'function'
      ? newValue(hook.value)
      : newValue;

    // Se o valor não mudou, não faz nada
    if (actualNewValue === hook.value) {
      return;
    }

    // Atualiza o valor
    hook.value = actualNewValue;

    // Reinicializa o índice de hooks
    hookIndex = 0;

    // Renderiza novamente o componente
    const currentHooks = componentHooks.get(currentComponent);
    const componentElement = currentComponent({});

    // Encontra o container anterior e limpa
    const container = document.getElementById('root');
    container.innerHTML = '';

    // Renderiza novamente
    render(componentElement, container);
  };

  return [hook.value, hook.setState];
}

// Exemplo prático:
function Contador() {
  const [count, setCount] = useState(0);

  return createElement('div', { className: 'contador' },
    createElement('p', null, `Contador: ${count}`),
    createElement('button', { 
      onClick: () => setCount(count + 1) 
    }, 'Incrementar')
  );
}

// Uso:
render(createElement(Contador, {}), document.getElementById('root'));

useEffect: Efeitos Colaterais e Cleanup

useEffect permite executar código após o componente ser renderizado. É útil para requisições, subscrições, e limpeza. Ele aceita uma função e um array de dependências — o efeito é executado quando qualquer dependência muda (ou em toda renderização, se não houver dependências).

function useEffect(callback, dependencies) {
  const hooks = componentHooks.get(currentComponent);
  const index = hookIndex;
  hookIndex++;

  // Inicializa o hook se necessário
  if (!hooks[index]) {
    hooks[index] = {
      cleanup: null,
      dependencies: null
    };
  }

  const hook = hooks[index];
  const hasNoDependencies = !dependencies;
  const dependenciesChanged = !hook.dependencies || 
    dependencies.some((dep, i) => dep !== hook.dependencies[i]);

  // Executa o efeito se não há dependências ou se elas mudaram
  if (hasNoDependencies || dependenciesChanged) {
    // Limpa o efeito anterior, se existir
    if (hook.cleanup) {
      hook.cleanup();
    }

    // Executa o novo efeito
    const cleanup = callback();
    hook.cleanup = typeof cleanup === 'function' ? cleanup : null;
    hook.dependencies = dependencies;
  }
}

// Exemplo: fetch de dados
function Usuario({ userId }) {
  const [usuario, setUsuario] = useState(null);
  const [carregando, setCarregando] = useState(true);

  useEffect(() => {
    // Simula uma requisição
    setCarregando(true);
    setTimeout(() => {
      setUsuario({ id: userId, nome: 'João Silva' });
      setCarregando(false);
    }, 500);

    // Cleanup (opcional)
    return () => {
      console.log('Componente foi desmontado ou dependências mudaram');
    };
  }, [userId]); // Reexecuta quando userId muda

  if (carregando) {
    return createElement('p', null, 'Carregando...');
  }

  return createElement('div', null,
    createElement('h2', null, usuario.nome),
    createElement('p', null, `ID: ${usuario.id}`)
  );
}

Colocando Tudo Junto: Um Exemplo Completo

Agora vamos integrar tudo em um exemplo funcional que demonstra createElement, render e hooks trabalhando juntos.

// Sistema de hooks
let currentComponent = null;
let componentHooks = new Map();
let hookIndex = 0;

function useState(initialValue) {
  const hooks = componentHooks.get(currentComponent);
  const index = hookIndex;
  hookIndex++;

  if (!hooks[index]) {
    hooks[index] = {
      value: typeof initialValue === 'function' ? initialValue() : initialValue,
      setState: null
    };
  }

  const hook = hooks[index];

  hook.setState = (newValue) => {
    const actualNewValue = typeof newValue === 'function'
      ? newValue(hook.value)
      : newValue;

    if (actualNewValue === hook.value) return;

    hook.value = actualNewValue;
    hookIndex = 0;

    const container = document.getElementById('root');
    container.innerHTML = '';
    const componentElement = currentComponent({});
    render(componentElement, container);
  };

  return [hook.value, hook.setState];
}

function useEffect(callback, dependencies) {
  const hooks = componentHooks.get(currentComponent);
  const index = hookIndex;
  hookIndex++;

  if (!hooks[index]) {
    hooks[index] = { cleanup: null, dependencies: null };
  }

  const hook = hooks[index];
  const hasNoDependencies = !dependencies;
  const dependenciesChanged = !hook.dependencies || 
    dependencies.some((dep, i) => dep !== hook.dependencies[i]);

  if (hasNoDependencies || dependenciesChanged) {
    if (hook.cleanup) hook.cleanup();
    const cleanup = callback();
    hook.cleanup = typeof cleanup === 'function' ? cleanup : null;
    hook.dependencies = dependencies;
  }
}

// Core functions
function createElement(type, props, ...children) {
  const elementProps = props || {};
  const flatChildren = children.flat();

  if (flatChildren.length > 0) {
    elementProps.children = flatChildren.length === 1 
      ? flatChildren[0] 
      : flatChildren;
  }

  return { type, props: elementProps };
}

function render(elemento, container) {
  if (typeof elemento === 'string' || typeof elemento === 'number') {
    container.appendChild(document.createTextNode(elemento));
    return;
  }

  if (Array.isArray(elemento)) {
    elemento.forEach(el => render(el, container));
    return;
  }

  if (!elemento) return;

  const { type, props } = elemento;

  if (typeof type === 'string') {
    const domElement = document.createElement(type);

    Object.entries(props).forEach(([key, value]) => {
      if (key === 'children') {
        if (value) render(value, domElement);
      } else if (key.startsWith('on')) {
        const eventName = key.substring(2).toLowerCase();
        domElement.addEventListener(eventName, value);
      } else if (key !== 'key') {
        domElement.setAttribute(key, value);
      }
    });

    container.appendChild(domElement);
  } else if (typeof type === 'function') {
    currentComponent = type;
    hookIndex = 0;

    if (!componentHooks.has(type)) {
      componentHooks.set(type, []);
    }

    const componentElement = type(props);
    render(componentElement, container);
  }
}

// Componentes
function App() {
  const [mensagem, setMensagem] = useState('Bem-vindo ao Mini React!');
  const [cliques, setCliques] = useState(0);

  useEffect(() => {
    console.log(`Cliques: ${cliques}`);
  }, [cliques]);

  return createElement('div', { style: 'text-align: center; padding: 20px;' },
    createElement('h1', null, mensagem),
    createElement('p', null, `Você clicou ${cliques} vezes`),
    createElement('button', {
      onClick: () => setCliques(cliques + 1),
      style: 'padding: 10px 20px; font-size: 16px; cursor: pointer;'
    }, 'Clique aqui')
  );
}

// Inicializa
render(createElement(App, {}), document.getElementById('root'));

HTML para testar:

<!DOCTYPE html>
<html lang="pt-BR">
<head>
  <meta charset="UTF-8">
  <title>Mini React</title>
</head>
<body>
  <div id="root"></div>
  <script src="mini-react.js"></script>
</body>
</html>

Conclusão

Implementar um mini React do zero ensina três lições fundamentais que transformam sua compreensão sobre a biblioteca. Primeiro, elementos são apenas objetos JavaScript — não há magia, apenas descrições de UI que o React interpreta. Compreender que JSX é açúcar sintático para createElement remove muito da "mágica negra" que envolta React. Segundo, hooks funcionam através de rastreamento de índice — a ordem importa porque React usa a posição do hook na renderização para identificá-lo, não nomes. Isso explica por que não podemos usar hooks condicionalmente. Terceiro, renderização é recursiva e componentes são funções — quando você entende que um componente é simplesmente uma função que retorna um elemento, e que esse elemento é renderizado recursivamente, a maioria dos comportamentos estranhos do React fazem sentido perfeito.

Este exercício também revela limitações importantes da nossa implementação (sem virtual DOM, sem diffing eficiente, sem limpeza de memória), que nos leva a apreciar a engenharia elegante que a equipe do React fez. Use esse conhecimento não apenas para entender React melhor, mas para fazer decisões melhores ao construir componentes.

Referências


Artigos relacionados