Módulo 3: State With Usestate
El estado es un snapshot
Descripción
Esta es la lección que más desconcierta a quien aprende React, y también la que —una vez la entiendes— hace que todo lo demás encaje. La afirmación es esta: el estado es un snapshot. Dentro de un render, la variable de estado no es una cosa viva que puedas ir cambiando; es una foto fija, una constante que vale lo mismo desde la primera línea del componente hasta la última.
La consecuencia suena a error, pero es exacta: si escribes setCount(count + 1) dos veces seguidas en el mismo handler, el contador sube 1, no 2. La primera vez que lo ves, parece un bug de React. No lo es: es el comportamiento correcto, y sale directamente de que count es un snapshot. En esta lección lo vas a medir con código ejecutado, no a creértelo de palabra, y al final te va a parecer obvio.
Desglosemos la idea. Cuando React ejecuta tu componente para un render, toma el valor actual del estado —digamos count = 0— y con él construye la variable count de esa pasada como una constante. Todo lo que ocurra en ese render lee ese 0: si a media función haces setCount(1), la variable count de esta pasada sigue valiendo 0. El 1 no aparece aquí; aparece en el siguiente render, cuando React vuelva a ejecutar el componente y construya una count nueva, esta vez valiendo 1. Cada render tiene su propia foto del estado, congelada, que no cambia a mitad de camino.
Conexión con el módulo. El snapshot es la pieza que explica dos cosas que ya rozaste. Explica por qué, en la lección 3, "el valor nuevo aparece en el siguiente render, no en la línea siguiente al setter": porque la variable de esta pasada es una foto fija. Y prepara la lección 6: el updater funcional (setCount(c => c + 1)) existe precisamente para los casos en que el snapshot te estorba —cuando necesitas basar el nuevo valor en el anterior sin quedar atrapado en la foto—. Sin entender el snapshot, el updater parece magia arbitraria; con el snapshot claro, es la solución evidente. Esta lección es el "por qué"; la 6 es el "entonces qué hago".
Una analogía: la foto instantánea que capturó el momento
Imagina que en una fiesta alguien te toma una foto instantánea —de esas que salen impresas al momento—. La foto capta cómo estabas en ese instante: de pie junto a la ventana, con la copa en la mano. Ahora tú te mueves, caminas al otro lado del salón, dejas la copa. Pero la foto no cambia: sigue mostrándote junto a la ventana con la copa, para siempre, porque capturó un momento y lo congeló. Si alguien mira esa foto una hora después, ve dónde estabas cuando se tomó, no dónde estás ahora.
El estado dentro de un render es esa foto. Cuando React ejecuta el componente, "toma la foto" del estado: count vale 0, y esa foto queda fija durante todo el render. Si a media función llamas setCount(1) —el equivalente a tú moviéndote por el salón—, la foto de este render no se actualiza: count sigue siendo 0 aquí, porque la foto ya se tomó. El 1 es una foto nueva, que se tomará en el próximo render. Dos personas mirando dos fotos distintas: el render actual ve 0, el siguiente verá 1.
Aquí está el giro que explica el "1, no 2". Si en la fiesta te toman dos fotos seguidas sin que te muevas, las dos salen iguales: tú junto a la ventana. Cuando escribes setCount(count + 1) dos veces en el mismo handler, las dos leen el mismo snapshot (count = 0), así que las dos calculan 0 + 1 = 1 y las dos piden "pon el estado en 1". No es "1 y luego 2"; es "1 y otra vez 1". El resultado es 1. Guarda la foto instantánea que no cambia aunque tú te muevas; es el corazón de esta lección.
Ejemplo trabajado: dos setCount(count + 1) que suman 1
Vamos a medir el comportamiento que parece imposible. Modelamos lo que React hace por dentro: durante un render, count es una constante (el snapshot), y cada setCount(next) agenda un valor en una cola que React procesará para el próximo render. Con eso reproducimos, exacto, por qué dos setCount(count + 1) suman 1.
Así se ve en React de verdad. Este handler parece que debería sumar 2, y es la trampa clásica:
import { useState } from 'react';
function CartBadge() {
const [count, setCount] = useState(0);
function handleAddTwo() {
setCount(count + 1); // count es 0 en este render -> agenda 1
setCount(count + 1); // count SIGUE siendo 0 aqui -> agenda 1 otra vez
// resultado: el estado queda en 1, no en 2
}
return <span>Cart ({count})</span>;
// (el boton que llama a handleAddTwo es el modulo 4)
}
Y la versión ejecutable en Node. Hacemos explícito que count es una constante del render y que setCount agenda un valor en una cola; luego procesamos la cola como React (cada valor reemplaza al estado):
'use strict';
// ── El estado es un SNAPSHOT por render ──
// Durante UN render, `count` es un valor FIJO (la foto de este render).
// setCount(count + 1) no cambia `count` aqui: agenda "poner el estado en
// (ese valor fijo) + 1" para el PROXIMO render.
const count = 0; // el snapshot: count vale 0 en TODO este render
const queue = []; // la cola de valores agendados para el proximo render
const setCount = (next) => queue.push(next);
// El handler del boton "+2": dos veces setCount(count + 1).
setCount(count + 1); // agenda 1 (count es 0 en este snapshot)
console.log('despues del 1er setCount, count sigue siendo:', count);
setCount(count + 1); // agenda 1 OTRA VEZ (count SIGUE siendo 0 aqui)
console.log('despues del 2do setCount, count sigue siendo:', count);
console.log('\ncola agendada por 2x setCount(count + 1):', JSON.stringify(queue));
// React procesa la cola: cada valor REEMPLAZA al estado anterior.
let state = count;
for (const v of queue) state = v;
console.log('\nesperado (intuicion equivocada): count + 2 =', count + 2);
console.log('real (por el snapshot): ', state);
// ── Por que: `count` nunca cambia dentro de un render ──
// Aunque llames a setCount mil veces, la variable `count` de ESTA pasada
// quedo "congelada" en 0. Es una constante local del render.
console.log('\n=== 3 llamadas: mismo efecto, todas leen el mismo snapshot ===');
const count2 = 5;
const queue2 = [];
const setCount2 = (next) => queue2.push(next);
setCount2(count2 + 1);
setCount2(count2 + 1);
setCount2(count2 + 1);
let state2 = count2;
for (const v of queue2) state2 = v;
console.log('count (snapshot):', count2, '| cola:', JSON.stringify(queue2), '| resultado:', state2);
Qué esperar. Al correr el archivo, la salida es exactamente esta:
despues del 1er setCount, count sigue siendo: 0
despues del 2do setCount, count sigue siendo: 0
cola agendada por 2x setCount(count + 1): [1,1]
esperado (intuicion equivocada): count + 2 = 2
real (por el snapshot): 1
=== 3 llamadas: mismo efecto, todas leen el mismo snapshot ===
count (snapshot): 5 | cola: [6,6,6] | resultado: 6
Aquí está el "misterio" resuelto, medido línea por línea.
Mira las dos primeras líneas: despues del 1er setCount, count sigue siendo: 0 y lo mismo tras el segundo. Esto es el snapshot en su forma más cruda. Llamamos a setCount —le pedimos a React que cambie el estado— y aun así, en la línea siguiente, count sigue valiendo 0. No cambió. No porque setCount no funcione, sino porque count es la foto de este render, y las fotos no se editan a mitad de camino. El valor nuevo vive en la cola, esperando el próximo render; la variable count de esta pasada es intocable.
Ahora la cola: [1,1]. Los dos setCount(count + 1) agendaron el mismo valor, 1, porque los dos leyeron el mismo count (que era 0). No fue "agenda 1, luego agenda 2"; fue "agenda 1, agenda 1 otra vez", porque count no cambió entre las dos llamadas. Dos fotos iguales de alguien que no se movió.
Y el desenlace: esperado 2, real 1. La intuición equivocada calcula "sumé 1 dos veces, van 2". Lo que de verdad pasa: React procesa la cola [1, 1] reemplazando el estado con cada valor —lo pone en 1, y luego lo vuelve a poner en 1—, y termina en 1. Cada setCount(valor) reemplaza, no acumula. Por eso dos setCount(count + 1) desde count = 0 dan 1.
El segundo bloque confirma que no es casualidad del 0: con count = 5, tres setCount(count + 1) agendan [6, 6, 6] (los tres leen el snapshot 5) y el resultado es 6, no 8. Da igual cuántas veces llames a setCount(count + 1) en un render: como todas leen la misma foto, todas agendan el mismo valor, y el resultado es "el snapshot + 1", una sola vez.
Por qué React hace esto (y por qué es bueno)
Podría parecer una rareza incómoda, pero el snapshot es una garantía valiosa, la misma que perseguimos desde el módulo 1: la predecibilidad. Que el estado sea constante durante un render significa que puedes leer el componente de arriba a abajo confiando en que count vale lo mismo en todas partes. No tienes que rastrear "¿en qué línea se volvió 1?", porque no se vuelve nada a mitad de render: es 0 en la línea 3 y es 0 en la línea 30. El JSX que devuelves, los cálculos que haces, los handlers que defines: todos ven la misma foto coherente del estado. Un render es una unidad atómica, con una foto fija de los datos.
Imagina el caos alternativo: si count pudiera cambiar a media función cada vez que llamas a setCount, el mismo count valdría cosas distintas en distintas líneas del mismo render, y tu UI sería una mezcla incoherente de valores viejos y nuevos según el orden exacto de ejecución. El snapshot elimina esa clase entera de bugs. A cambio, te pide entender una cosa: para basar el nuevo estado en el anterior dentro del mismo render, no leas la variable (que está congelada) —usa el updater funcional (lección 6), que sí ve el valor pendiente—.
Dibujado, un render con su snapshot y la cola que alimenta al siguiente:
flowchart TD
S0["Estado: 0"] --> R1["Render: count = 0 (snapshot fijo)"]
R1 --> C1["setCount(count + 1) -> agenda 1"]
R1 --> C2["setCount(count + 1) -> agenda 1"]
C1 --> Q["cola: [1, 1]"]
C2 --> Q
Q --> S1["React procesa: reemplaza -> Estado: 1"]
S1 --> R2["Proximo render: count = 1 (foto nueva)"]
Fíjate en que count + 1 se calcula dentro del render, con el snapshot fijo, y produce el mismo 1 las dos veces. El estado nuevo no se aplica hasta que React procesa la cola, ya fuera de este render.
Errores comunes
Esperar leer el valor nuevo justo después del setter. Qué pasa: se escribe setCount(count + 1); console.log(count) esperando ver el valor incrementado, y sale el viejo. Por qué pasa: uno imagina setCount como una asignación instantánea (count = count + 1), que sí cambiaría la variable en el sitio. Cómo detectarlo: lees la variable de estado en la línea siguiente al setter y ves el valor de antes. Cómo corregirlo: interioriza que la variable de estado es el snapshot de este render y no cambia dentro de él. Si necesitas el valor nuevo en el mismo handler para un cálculo, guárdalo en una variable local (const next = count + 1; setCount(next); usar(next)), no lo leas del estado. El estado nuevo solo lo verás en el siguiente render.
Llamar a setX(x + 1) en un bucle esperando que acumule. Qué pasa: se hace for (let i = 0; i < 3; i++) setCount(count + 1) esperando sumar 3, y suma 1. Por qué pasa: parece que tres llamadas deberían sumar tres. Cómo detectarlo: un bucle (o varias llamadas seguidas) de setX(x + valor) produce un solo incremento, no la suma. Cómo corregirlo: como count es el mismo snapshot en las tres vueltas, las tres agendan el mismo valor. Para acumular sobre el valor pendiente, usa el updater funcional: for (...) setCount(c => c + 1) —cada updater ve el resultado del anterior— (lección 6). O calcula el total de una vez y haz un solo setCount(count + 3). Nunca esperes que setX(x + 1) repetido se sume: el snapshot lo impide.
Creer que es un bug de React. Qué pasa: alguien concluye que "React está roto" porque setCount(count + 1) dos veces no suma 2. Por qué pasa: el comportamiento choca con la intuición imperativa. Cómo detectarlo: buscas "workarounds" o setTimeout para "forzar" que el segundo set vea el primero. Cómo corregirlo: no es un bug; es el diseño, y es deseable (te da un estado coherente durante todo el render). El "arreglo" correcto no es forzar nada, sino elegir la herramienta adecuada: si el nuevo valor depende del anterior, el updater funcional (lección 6) es la respuesta diseñada para eso. Pelear contra el snapshot con hacks produce código frágil; entenderlo y usar el updater produce código correcto.
Ejercicios
Ejercicio 1 — Predice el resultado. Sin correr nada, di en cuánto queda count tras ejecutar cada handler, partiendo de count = 10 en el render actual:
// (a)
setCount(count + 1);
// (b)
setCount(count + 5);
setCount(count + 5);
// (c)
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
Ver solución
Con count = 10 fijo (el snapshot) en todo el render:
- (a) Agenda
11. Resultado: 11. (Una sola llamada, sin sorpresa.) - (b) Los dos
setCount(count + 5)leencount = 10y agendan15los dos: cola[15, 15]. Cada uno reemplaza. Resultado: 15, no 20. Ambos calcularon10 + 5. - (c) Los tres leen
count = 10y agendan11los tres: cola[11, 11, 11]. Resultado: 11, no 13. Todos calcularon10 + 1sobre el mismo snapshot.
La clave en (b) y (c): count no cambia entre las llamadas; es la foto del render. Cada setCount(valor) reemplaza con el mismo valor calculado, así que el resultado es "snapshot + incremento", una sola vez.
Ejercicio 2 — Explica la salida. En el ejemplo trabajado, tras los dos setCount(count + 1), el log dijo count sigue siendo: 0 las dos veces. Explica con tus palabras por qué count no cambió, aunque llamamos al setter, y qué habría que hacer para "ver" el valor nuevo.
Ver solución
count no cambió porque es el snapshot de este render: cuando React ejecutó el componente, construyó count como una constante con el valor 0, y esa constante no se puede reasignar a media función. setCount(count + 1) no toca la variable count; lo que hace es agendar un valor nuevo en la cola para que React arme el próximo render. Por eso, en la línea siguiente al setter, count sigue siendo 0: seguimos dentro del mismo render, mirando la misma foto.
Para "ver" el valor nuevo hay dos caminos. Uno: esperar al siguiente render —React lo ejecutará con count valiendo el nuevo valor, y ahí sí lo lees—. Dos: si necesitas el valor nuevo dentro del mismo handler (por ejemplo, para pasarlo a otra función), no lo leas del estado; guárdalo en una variable local: const next = count + 1; setCount(next); hacerAlgoCon(next). La variable de estado es de solo lectura y congelada en el render; el valor nuevo lo tienes tú, en tu cálculo, o lo verás en la próxima pasada.
Ejercicio 3 — Diseña el handler correcto. Quieres un botón "Add 3" que sume 3 al contador del carrito de una sola vez. Un compañero propone setCount(count + 1); setCount(count + 1); setCount(count + 1). Explica por qué eso suma 1 y no 3, y escribe dos maneras correctas de sumar 3.
Ver solución
Por qué suma 1: los tres setCount(count + 1) leen el mismo snapshot de count. Si count era, digamos, 0, los tres calculan 0 + 1 = 1 y agendan 1; la cola es [1, 1, 1] y cada valor reemplaza al anterior, así que el estado termina en 1. No acumula porque count no cambia entre las llamadas.
Manera correcta 1 — un solo setCount con la suma completa:
setCount(count + 3); // agenda "snapshot + 3" de una vez
Simple y directo: como sabes que quieres +3, lo calculas de una vez sobre el snapshot.
Manera correcta 2 — el updater funcional (lección 6):
setCount(c => c + 1);
setCount(c => c + 1);
setCount(c => c + 1);
Aquí pasas funciones en vez de valores. React las aplica en orden sobre el valor pendiente: 0 → 1 → 2 → 3. Cada updater ve el resultado del anterior, no el snapshot, así que sí acumula. Esta es la herramienta diseñada para "basar el nuevo valor en el anterior", y la estudiamos a fondo en la próxima lección.
Las dos dan count = 3. La primera sirve cuando conoces el incremento total; la segunda, cuando cada paso depende del anterior o vienen de lugares distintos.
Resumen y siguiente paso
En esta lección mediste la propiedad más contraintuitiva del estado: es un snapshot. Dentro de un render, la variable de estado es una constante fija —la foto de ese instante—, y no cambia aunque llames al setter. Lo comprobaste ejecutando: tras setCount(count + 1), count seguía valiendo 0; dos llamadas agendaron [1, 1] (las dos leyeron el mismo snapshot); y el resultado fue 1, no 2, porque cada setCount(valor) reemplaza. Entendiste por qué React lo hace —para darte un render coherente, con una sola foto del estado de arriba a abajo— y qué implica: para basar el nuevo valor en el anterior dentro de un render, la variable congelada no sirve; hace falta otra herramienta.
Antes de avanzar deberías poder: explicar qué es el snapshot con la foto instantánea; predecir el resultado de varias llamadas a setX(x + n) en un render (todas leen la misma foto); saber por qué la variable de estado no cambia justo después del setter; y reconocer que "no es un bug", sino el diseño que da coherencia al render.
La lección 5 toma la otra mitad de la mecánica: qué pasa después de que agendaste el cambio. Vas a ver que actualizar el estado dispara un re-render —React vuelve a ejecutar el componente con la foto nueva, y produce una UI nueva (UI = f(estado))—, y a trazar la secuencia de renders de una máquina de estado. Snapshot (esta lección) más re-render (la siguiente) son las dos mitades de cómo se comporta el estado cuando lo cambias.
Recursos
- React, "State as a Snapshot" — react.dev/learn/state-as-a-snapshot. La página oficial de esta lección: por qué la variable de estado es una foto fija del render y por qué el valor no cambia hasta el siguiente. Léela con el ejemplo ejecutado al lado. En inglés.
- React, "Queueing a Series of State Updates" — react.dev/learn/queueing-a-series-of-state-updates. Cómo React encola las actualizaciones y por qué varias
setX(x + 1)en un render suman una vez; conecta el snapshot con el updater de la lección 6. En inglés. - React, "Render and Commit" — react.dev/learn/render-and-commit. El ciclo de render de React: cada render es una unidad con su propia foto del estado. En inglés.
- MDN, "const" — developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/const. Por qué una constante no se reasigna a media función; la base de JavaScript detrás de "el estado es una constante del render". En inglés.