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.
1queue.pending ──────────────┐2 ▼3 ┌──▶ update1 ──▶ update2 ──▶ update3 ──┐4 └──────────────────────────────────────┘5 6// insertar al final: O(1)7// recorrer en orden de llegada: empezar en pending.nextCon 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 / 81Sin 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 / 81Sin 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 / 35Sin 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 nuevaAntes 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?