Se você tá afim de revisar base de React e entender, de maneira mais profunda, como essa biblioteca funciona por debaixo dos panos e consegue ser tão performática, esse artigo é pra você! Motivado por não ter conseguido responder à pergunta: "Você saberia explicar como funciona o algoritmo de reconciliação do React?", hoje escrevo esse artigo, pra que você saiba responder à pergunta que eu não soube.

Do que se trata esse algoritmo afinal?

O algoritmo de reconciliação do React é o mecanismo que a biblioteca tem para determinar como as mudanças no estado de uma aplicação devem refletir na interface do usuário de forma eficiente e performática. E muita atenção ao "eficiente e performática" aqui, porque quando um estado muda, em vez de toda a nossa árvore de componentes HTML ser recriada do zero, o React comparará:

  • A nova árvore de elementos, criada e renderizada na "Virtual DOM"
  • Com a árvore antiga, que já está lá no browser

Aplicando apenas as mudanças DE FATO NECESSÁRIAS no DOM real.

None
Imagem retirada do artigo https://medium.com/@ryanbas21/react-reconciliation-7075e3f07437

Se você está familiarizado com o HTTP, a ideia aqui é bem o que acontece com o método PATCH do protocolo, no sentido de apenas serem aplicadas O QUE DE FATO MUDOU na comparação entre a DOM Virtual e a DOM real. Isso, assim como o método PATCH do Hypertext Transfer Protocol, que atualiza apenas os dados que foram enviados na requisição, no recurso a ser alterado do servidor.

"Pera aí que eu já fiquei confuso! Virtual O QUE?"

Virtual DOM! Calma que eu explico. A DOM Virtual é uma representação leve e em memória da árvore de elementos da DOM real. Ela permite que o React compare diferentes estados da interface de forma eficiente, identificando mudanças entre:

  • A versão antiga
  • E a nova

Sem qualquer necessidade de interagir diretamente com a DOM real, o que só acontece quando de fato é necessário aplicar alguma atualização. Isso reduz o custo e a complexidade das operações de atualização da interface, tornando o processo mais rápido e eficiente🐇⚡

*Você já deve ter ouvido falar que aplicações React são performáticas por natureza e elas são mesmo e, agora, você sabe o motivo.

Bora pro código! Exemplo simples de gerenciamento de estado, você já viu isso 30 mil vezes na sua vida:

export function App() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <h1>Contador: {count}</h1>
      <button onClick={() => setCount(count + 1)}>Incrementar</button>
    </div>
  );
}

Quando o estado count muda, o React precisa atualizar a interface para refletir esse novo estado. Mas, como ele decide o que atualizar? É aqui que o algoritmo entra em ação😎

Como o algoritmo funciona?

O processo de reconciliação ocorre em duas fases principais:

  • A Renderização
  • E o Commit.

1. Renderização

Aqui o React "reage", criando uma nova árvore de elementos com base nas alterações do estado. Essa nova árvore é comparada com a árvore anterior, que está lááá no browser.

Então, por exemplo:

<div>
  <h1>Contador: 0</h1>
  <button>Incrementar</button>
</div>

Considera que a árvore de elementos que está no navegador é essa aqui em cima☝️

E, após uma mudança de estado, o React vai renderizar isso aqui:

<div>
  <h1>Contador: 1</h1>
  <button>Incrementar</button>
</div>

Fazendo a comparação entre as duas DOMs, o React estará apto a identificar que apenas o conteúdo dentro do <h1> mudou.

2. Commit

Após a comparação, é feito o commit, isto é, o React aplica APENAS O QUE DE FATO MUDOU na DOM real, no nosso caso, o texto dentro do <h1>. Com isso, o restante da árvore de elementos permanecerá intacta, o que torna o processo muito mais eficiente.

Se fosse pra demonstrar em um código, o que o React executa para mudar o que aparece em tela, seria "bais ou benos" isso:

document.querySelector('h1').textContent = 'Contador: 1';
None

Componentes de Classe vs Componentes Funcionais nessa brincadeira

O algoritmo de reconciliação lida de maneira ligeiramente diferente com componentes de classe e funcionais. Embora a lógica interna do algoritmo seja a mesma, o React lida com o estado e o ciclo de vida de maneira diferente dependendo do tipo de componente.

Componentes de classe

Em componentes de classe, além do que falamos, o React também vai levar em conta métodos como o shouldComponentUpdate() para otimizar o processo de reconciliação. Esse método, por exemplo, tem o poder de dizer para o React renderizar, ou não, um componente. A sua chamada no momento certo pode evitar que o processo de renderização — o qual falamos agora a pouco — comece desnecessariamente, otimizando performance✨

Componentes funcionais

Falando de componentes funcionais, temos o memo(), que pode envolver esses tipos de componentes para que re-renderizações sejam evitadas enquanto as suas props não mudem, nisso, o algoritmo de reconciliação não é acionado e ganhamos eficiência. Além dele, temos o useMemo(), que tem o objetivo de evitar que cálculos e computações pesadas sejam feitas de novo a cada renderização. Nele, um valor é memorizado e, enquanto as suas dependências não mudarem, o seu valor também não muda. Isso evita que o processo de geração da DOM Virtual seja mais demorado, deixando as nossas aplicações mais rápidas ainda⚡

*Esses dois métodos estão bem relacionados a performance no React, caso você se interesse a pesquisar mais sobre🤓

None

O papel das "keys" na reconciliação

Para de ser safado e corrige esse erro cabra…

None
Imagem retirada do artigo https://engineering.blackrock.com/5-common-mistakes-with-keys-in-react-b86e82020052

Brincadeiras a parte… A key do React desempenha um papel crucial no processo de reconciliação, especialmente ao lidar com listas de elementos. Elas ajudam o React a identificar quais itens mudaram, foram adicionados ou removidos e, então, apenas os elementos que de fato sofreram alteração são renderizados em tela, de novo: para aumentar performance.

function TodoList({ todos }) {
  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  );
}

Considere o exemplo, cada item da lista possui uma key única, todo.id. Falando de listas, o React olha pra essa key para determinar precisamente quais itens atualizar, evitando a renderização de toda a lista, ó:

Como o algoritmo trata arrays

Assim que o React encontra um array de elementos, as keys são usadas para determinar quais itens mudaram em uma remoção, ou reordenação de itens, por exemplo. Considerando que essas todos fossem passados para o componente que vimos antes:

const todos = [
  { id: 1, text: 'Comprar leite' },
  { id: 2, text: 'Lavar o carro' },
  { id: 3, text: 'Estudar React' }
];

Se fossemos remover o item de id=2, em uma filtragem, por exemplo:

const updatedTodos = todos.filter(todo => todo.id !== 2);

O React consegue saber, através das keys, exatamente qual item foi removido (o de id=2), nisso, ele vai lá e ajusta a DOM de acordo, como um maestro.

None

Conclusão

Entender o algoritmo de reconciliação é fundamental para escrever componentes mais eficientes e evitar problemas de desempenho. Saber como o React toma decisões internas pode te ajudar a otimizar sua aplicação e evitar armadilhas comuns, como renderizações desnecessárias ou mudanças inesperadas no DOM. Isso, além de fazer você codar de uma forma mais consciente do que acontece por debaixo dos panos.

Bom, é isso, espero que tenha gostado! E se tiver alguma sugestão deixe aí nos comentários 💬

Se gostou, dê 1 ou 50 claps (Só clicar 50x na 👏), ou é 1, ou é 50, não tem meio termo.

Obrigado pela leitura!

Me acompanhe por aí! 😜