Saltar al contenido

La cola de updates y el batching

Llamo a setState tres veces seguidas. ¿Cuántas veces renderiza?

Al terminar vas a poder explicar

  • Explicar por qué setN(n+1) tres veces suma uno y con updater suma tres
  • Describir cuándo React agrupa updates y cuándo no
  • Reconocer el caso en que setState no dispara ningún render

El nombre setState es una mentira piadosa. No setea nada. Crea un objeto update, lo encola, y avisa que hay trabajo pendiente. El estado no cambia hasta el próximo render.

Todo lo raro que hace setState — que llamarlo tres veces sume uno, que a veces no haga nada, que el valor que leés justo después sea el viejo — se explica con esa sola frase.

La cola es circular

Cada hook de estado tiene una queue. Los updates se guardan ahí en una lista enlazada circular, donde queue.pending apunta al último y pending.next al primero.

tres updates encolados
1queue.pending ──────────────┐23  ┌──▶ update1 ──▶ update2 ──▶ update3 ──┐4  └──────────────────────────────────────┘5 6// insertar al final: O(1)7// recorrer en orden de llegada: empezar en pending.next

Con un solo puntero se consigue insertar al final en tiempo constante y recorrer en orden de llegada. Un array daría lo mismo funcionalmente, pero costaría reasignaciones.

Valor contra función actualizadora

Acá está el malentendido más caro de React. Estos dos manejadores no hacen lo mismo, y la diferencia no es de estilo.

setN(n + 1) tres veces: suma uno

AGENDADOpaso 1 / 81
Traza
Cadena de hooks · <>

Sin hooks en este paso.

Árbol de fibers

  • <HostRoot>

Salida

Nada en el host todavía. React no commiteó ningún cambio.

createRoot().render()

Montaje inicial. No hay árbol previo, así que todo se va a crear desde cero.

VER EL CÓDIGO QUE SE ESTÁ TRAZANDO
1function TresValores() {2  const [n, setN] = useState(0);3 4  return (5    <button6      onClick={() => {7        setN(n + 1);   // n vale 0 en esta clausura8        setN(n + 1);   // sigue valiendo 09        setN(n + 1);   // sigue valiendo 010      }}11    >12      valor: {n}13    </button>14  );15}

Con el foco acá: ← → un paso · espacio reproduce · Inicio/Fin van a los extremos.

Mirá los pasos hook.applyUpdate. Los tres updates guardaron el valor 1, porque n valía 0 en la clausura del manejador y sigue valiendo 0 hasta el próximo render. Al procesar la cola, React aplica 1, después 1 otra vez, y después 1 otra vez. Resultado: 1.

setN(v => v + 1) tres veces: suma tres

AGENDADOpaso 1 / 81
Traza
Cadena de hooks · <>

Sin hooks en este paso.

Árbol de fibers

  • <HostRoot>

Salida

Nada en el host todavía. React no commiteó ningún cambio.

createRoot().render()

Montaje inicial. No hay árbol previo, así que todo se va a crear desde cero.

VER EL CÓDIGO QUE SE ESTÁ TRAZANDO
1function TresFunciones() {2  const [n, setN] = useState(0);3 4  return (5    <button6      onClick={() => {7        setN((v) => v + 1);   // v = 0 → 18        setN((v) => v + 1);   // v = 1 → 29        setN((v) => v + 1);   // v = 2 → 310      }}11    >12      valor: {n}13    </button>14  );15}

Con el foco acá: ← → un paso · espacio reproduce · Inicio/Fin van a los extremos.

Ahora los updates guardaron funciones. React las aplica en cadena, pasándole a cada una el resultado de la anterior: 0 → 1 → 2 → 3. La función no lee tu clausura: lee el valor acumulado en la cola.

El batching, y qué cambió en React 18

¿Por qué los tres updates produjeron un render y no tres? Porque React abre un lote alrededor del manejador de eventos: todo lo que se encole adentro se procesa junto.

Hasta React 17 ese lote existía solamente dentro de manejadores de eventos. Si llamabas setState dos veces dentro de un setTimeout, una promesa o un efecto, tenías dos renders completos. React 18 cambió eso: el lote se abre siempre. Se llama batching automático.

1function alHacerClick() {2  setContador(c => c + 1);3  setBandera(b => !b);4}5// React 17 → 1 render (estamos en un evento)6// React 18 → 1 render7 8setTimeout(() => {9  setContador(c => c + 1);10  setBandera(b => !b);11}, 0);12// React 17 → 2 renders  😖13// React 18 → 1 render   ✓

En el comparador de versiones podés correr el mismo componente en los dos modos y contar los renders.

Cuando setState no hace absolutamente nada

Si la cola está vacía y el fiber no tiene trabajo pendiente, React calcula el estado siguiente por adelantado. Si el resultado es idéntico al actual según Object.is, descarta el update y ni siquiera agenda un render.

setN(0) cuando n ya vale 0: cero renders

AGENDADOpaso 1 / 35
Traza
Cadena de hooks · <>

Sin hooks en este paso.

Árbol de fibers

  • <HostRoot>

Salida

Nada en el host todavía. React no commiteó ningún cambio.

createRoot().render()

Montaje inicial. No hay árbol previo, así que todo se va a crear desde cero.

VER EL CÓDIGO QUE SE ESTÁ TRAZANDO
1function MismoValor() {2  const [n, setN] = useState(0);3 4  // Setear el MISMO valor no dispara ningún render.5  return <button onClick={() => setN(0)}>sigue en {n}</button>;6}

Con el foco acá: ← → un paso · espacio reproduce · Inicio/Fin van a los extremos.

Buscá el paso eagerState === currentState. Ojo con la trampa: esto compara por identidad. Si tu estado es un objeto y lo mutás sin cambiar la referencia, React tampoco renderiza — porque para él nada cambió.

1const [config, setConfig] = useState({ tema: "oscuro" });2 3config.tema = "claro";4setConfig(config);        // ✗ misma referencia: React no ve ningún cambio5 6setConfig({ ...config, tema: "claro" });   // ✓ referencia nueva

Antes de seguir · predecí

¿Qué imprime este manejador, y con qué valor termina el contador?

1function Contador() {2  const [n, setN] = useState(0);3 4  function alClick() {5    setN(n + 5);6    setN(v => v + 1);7    console.log(n);8  }9 10  return <button onClick={alClick}>{n}</button>;11}

Lo que te llevás

setState encola. La cola se procesa en orden, en el próximo render. La función actualizadora lee el valor acumulado; el valor directo lee tu clausura. El lote decide cuántos renders hay. Y si el estado no cambia por identidad, no hay render en absoluto.

Antes de marcarla, comprobá

  • ¿Podés explicar explicar por qué setN(n+1) tres veces suma uno y con updater suma tres?
  • ¿Podés explicar describir cuándo React agrupa updates y cuándo no?
  • ¿Podés explicar reconocer el caso en que setState no dispara ningún render?