blog / aquele bug de re-render que na verdade é feature

aquele bug de re-render que na verdade é feature

read in English
  • Front-end
  • React

vamos direto ao código. o que você espera que aconteça aqui?

function handleClick() {
  setCount((c) => c + 1);
  setFlag((f) => !f);
  console.log("depois dos dois sets");
}

a resposta intuitiva costuma ser "dois renders, um pra cada set". faz sentido pensar assim, já que são duas chamadas de state separadas. só que não é isso que acontece, o react agrupa as duas atualizações e dispara um único re-render com os dois valores já atualizados. isso se chama batching.

pra quem tá começando: "render" é o momento em que o react redesenha a tela com os dados atuais. e "state" é só o nome bonito pra "as informações que o componente guarda e que, quando mudam, fazem a tela mudar junto" (tipo o contador de um botão de curtir).

por que isso importa na prática

não é só curiosidade de bastidor. isso afeta diretamente performance e, mais sutilmente, pode confundir quem espera ler um valor "no meio do caminho":

function handleClick() {
  setCount((c) => c + 1);
  console.log(count); // ainda mostra o valor antigo!
}

o count que você lê no console.log ainda é o valor de antes do clique. isso acontece porque atualizar o state não é uma coisa instantânea, é mais parecido com colocar um pedido numa fila: você "agenda" a mudança, e ela só aparece de verdade no próximo render. isso confunde muita gente que vem de código mais imperativo, tipo vanilla js, onde uma variável muda na hora, sem fila nem nada.

pensa assim: é como pedir pro garçom trocar seu prato. o pedido foi feito, mas o prato novo só chega depois, não no mesmo segundo que você pediu.

a pegadinha real: nem sempre foi assim em TODO lugar

até o react 17, esse batching só acontecia dentro de event handlers do react (onClick, onChange etc, ou seja: funções que rodam quando você clica ou digita algo). se você chamasse setState dentro de um setTimeout, de uma promise resolvida, ou de um callback nativo do browser, cada chamada disparava um render separado:

// react 17 e anteriores
setTimeout(() => {
  setCount((c) => c + 1); // render 1
  setFlag((f) => !f);     // render 2
}, 1000);

dois renders ali, sem escapatória, mesmo que o código pareça idêntico ao primeiro exemplo. a diferença estava só no contexto de onde o setState era chamado, algo que não dava pra adivinhar só lendo a função em isolado.

o react 18 resolveu isso introduzindo o automatic batching. agora, com createRoot (a forma moderna de iniciar uma aplicação react), praticamente qualquer lugar (setTimeout, promises, listeners nativos, o que for) se comporta igual ao event handler. um clique, uma promise resolvida, um timeout: tudo agrupa as atualizações da mesma forma.

e se eu não quiser agrupar?

existe uma válvula de escape pra isso: flushSync, do react-dom. ele força um render síncrono imediato, sem esperar o batching:

import { flushSync } from "react-dom";

function handleClick() {
  flushSync(() => {
    setCount((c) => c + 1);
  });
  // dom já atualizado aqui, pode ler direto
  console.log(divRef.current.textContent);
}

use com moderação. na prática, quase nunca você vai precisar disso, é mais um recurso pra casos raros (tipo medir o dom imediatamente após uma mudança) do que algo pra usar no dia a dia.

o que fica dessa história

  • setState não é instantâneo, ele entra numa fila (é "agendado")
  • múltiplos setState no mesmo momento tendem a virar um único render (batching)
  • antes do react 18, isso só valia dentro de handlers do próprio react, fora disso, cada chamada era um render separado
  • o react 18 unificou esse comportamento em quase todo lugar via automatic batching
  • flushSync existe pros casos raros onde você precisa mesmo de um render síncrono

no fim das contas, entender isso evita duas coisas: código que "deveria funcionar mas não funciona" (tipo ler state recém-atualizado direto após o set) e otimizações prematuras tentando "forçar" menos renders manualmente quando o react já tá fazendo isso por debaixo dos panos.

é código, mas também é meio filosófico: às vezes a ferramenta já resolveu o problema antes de você nem perceber que ele existia. bonito, né? =]