Rastreando una llamada a React setState desde la cola de actualizaciones hasta el registro en el DOM
Sigue paso a paso una actualización de estado en React a través de la cola de actualizaciones de Hooks, el programador, la fase de renderizado, la reconciliación y el registro, y comprende por qué el estado nunca cambia de inmediato.
Casi todo desarrollador de React termina escribiendo un setter de estado seguido de un console.log y se sorprende al ver que se imprime el valor anterior. El nombre setState sugiere una asignación inmediata, pero eso no es lo que hace React: al llamar a un setter se registra una solicitud de estado futuro, y todo un proceso se ejecuta antes de que algo llegue a la pantalla. Este artículo sigue una sola actualización a través de ese proceso, desde la cola de actualizaciones del Hook pasando por la programación, el renderizado, la reconciliación y la fase de confirmación. Una vez que pueda imaginar cada etapa, varios comportamientos que al principio parecen extraños se vuelven predecibles: por qué el estado parece asíncrono, por qué varias actualizaciones pueden reducirse a un solo renderizado, por qué React a veces omite tareas y por qué existen las actualizaciones funcionales.
El fragmento que sorprende a todos
Imagínese una revisión de código donde esto parece perfectamente razonable:
setCount(count + 1);
console.log(count);
Se espera que la consola muestre el valor incrementado. En cambio, muestra el anterior. Aquí está la misma situación dentro de un componente completo:
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
console.log(count);
};
return (
<button onClick={handleClick}>
{count}
</button>
);
}
Al hacer el primer clic, muchos desarrolladores esperan ver:
1
Lo que realmente aparece en la consola es:
0
La razón es que React aún no ha vuelto a renderizar. En el momento en que se ejecuta console.log, React solo ha recibido una solicitud. Nada en la cola aún se ha aplicado, la función del componente no se ha llamado nuevamente y el DOM no ha cambiado. El código que estás ejecutando pertenece a la renderización actual, y dentro de esa renderización count es una constante simple que se fijó cuando se ejecutó la función. Nada puede volver a asignarle un valor.
Ese es el primer cambio en el modelo mental: un setter no modifica la variable de estado que se está leyendo. Simplemente programa el trabajo para que React lo realice más tarde.
Qué almacena React cuando se llama a un setter
Cuando escribes:
setCount(count + 1);
es tentador imaginar que React hace algo así internamente:
count = count + 1
Pero no lo hace. React crea un objeto de actualización y lo agrega a una cola que pertenece a ese Hook específico. Conceptualmente, la situación después del clic se ve así:
Current State
|
▼
count = 0
|
▼
User Clicks
|
▼
setCount(1)
|
▼
Update Queue
[ Update: 1 ]
El estado en sí permanece intacto. Todo lo que ha hecho React es anotarse a sí mismo: la próxima vez que se procesen actualizaciones para este Hook, count debería convertirse en 1.
Por qué las actualizaciones pasan por una cola
Una cola tiene sentido cuando varias actualizaciones llegan muy seguidas. Considera un manejador que llama al setter tres veces:
const handleClick = () => {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
};
Alguien nuevo en React podría esperar que un solo clic produzca:
count = 3
El resultado real es:
count = 1
Todas las tres llamadas se realizaron durante el mismo render, y ese render mostró:
count = 0
Por lo tanto, cada count + 1 da como resultado el mismo número, y React recibe tres solicitudes idénticas:
setCount(1)
setCount(1)
setCount(1)
La cola contiene, por lo tanto, tres entradas que todas indican “establecer en 1”:
[1]
[1]
[1]
Procesarlas en orden sigue terminando con:
1
y no con:
3
La misma lógica se aplica cuando los valores difieren. Supongamos que un render realiza estas tres llamadas:
setCount(1)
setCount(6)
setCount(4)
La cola entonces contiene:
[1]
[6]
[4]
Cada entrada reemplaza por completo el estado anterior, por lo que después del procesamiento solo importa la última y el resultado es 4. Los valores simples son reemplazos, no instrucciones para construir sobre lo que había antes. Esa limitación es precisamente la razón por la cual existen las actualizaciones funcionales.
Las actualizaciones funcionales calculan a partir del estado más reciente
Ahora cambie el manejador para que cada llamada pase una función:
const handleClick = () => {
setCount(prev => prev + 1);
setCount(prev => prev + 1);
setCount(prev => prev + 1);
};
Esta vez la cola contiene algo más parecido a tres pequeños programas que a tres valores:
prev => prev + 1
prev => prev + 1
prev => prev + 1
Cuando React procesa la cola, pasa la salida de cada función a la siguiente:
0 → 1
1 → 2
2 → 3
Y el estado final es:
count = 3
La diferencia es que una función de actualización no captura un valor del renderizado actual. Describe cómo derivar el siguiente estado a partir del estado que React tiene en el momento en que procesa esa entrada. Utilice esta forma siempre que el nuevo estado dependa del anterior, especialmente cuando varias actualizaciones pueden estar en cola juntas o cuando la actualización ocurre dentro de una función de callback creada durante un renderizado anterior.
El ciclo de vida completo de una actualización
Teniendo en cuenta la cola, siga un único clic que llama a:
setCount(count + 1);
Una visión simplificada de todo lo que ocurre a continuación:
User Click
|
▼
Create Update
|
▼
Place Update Into Queue
|
▼
Notify React Scheduler
|
▼
Schedule Render
|
▼
Render Phase
|
▼
Reconciliation
|
▼
Commit Phase
|
▼
DOM Updated
Las secciones siguientes explican cada etapa.
Paso 1: se crea un registro de actualización
Llamar a setCount(1) nunca vuelve a renderizar nada directamente:
setCount(1);
React crea un registro de actualización, que puede considerarse como una nota que dice:
Apply this update later.
Ese registro está asociado al estado interno del Hook. Los datos del Hook coexisten junto con la Fiber del componente, el nodo interno que React mantiene para cada instancia de componente, y la cola de actualizaciones también se encuentra allí:
Fiber
|
└── useState
|
├── Current State
└── Update Queue
Por esta razón también es necesario llamar a los Hooks en el mismo orden en cada renderizado: React encuentra el estado y la cola de cada Hook según su posición en esa lista.
Paso 2: React programa el trabajo
Al contar con una actualización, React debe decidir cuándo procesarla. Esa es la función del programador de tareas. React no necesariamente renderiza en el instante en que se llama a un setter; equilibra la respuesta rápida con la realización de trabajos innecesarios.
Imagínese a un usuario escribiendo rápidamente en un campo:
A
AB
ABC
ABCD
ABCDE
Renderizar de forma síncrona las partes más costosas de la interfaz de usuario después de cada tecla pulsada, sin posibilidad de priorizar, haría que las pantallas complejas parecieran lentas. La programación permite a React combinar las actualizaciones que llegan al mismo tiempo y, gracias a características concurrentes como las transiciones, tratar las actualizaciones urgentes, como la entrada en sí, de manera diferente a las menos urgentes, como una lista de resultados filtrados. Esa capacidad es en gran parte la razón por la cual la arquitectura de React pasó a utilizar actualizaciones programadas en lugar de inmediatas.
Paso 3: la fase de renderizado ejecuta el componente nuevamente
Cuando React decide que ha llegado el momento de procesar las actualizaciones pendientes, inicia un nuevo renderizado. Renderizar simplemente significa llamar nuevamente a la función del componente:
function Counter() {
const [count, setCount] = useState(0);
return <h1>{count}</h1>;
}
El detalle importante es lo que hace useState durante esta llamada. Antes de devolver un valor, procesa las actualizaciones en cola con respecto al estado anterior. Comenzando por:
Previous State = 0
React recorre la cola:
Queue:
[ +1 ]Process QueueResult:
1
y el valor que useState devuelve ahora es:
count = 1
que el componente utiliza para esta renderización. Por eso el nuevo valor solo se vuelve visible en la siguiente renderización: se calcula allí, no en el momento en que se llama al setter.
Paso 4: se genera un nuevo árbol de elementos
Ejecutar el componente devuelve un árbol fresco de elementos de React. Junto con la renderización anterior, se ve así:
Previous Render
<h1>0</h1>
New Render
<h1>1</h1>
El DOM del navegador aún no ha sido modificado. Todo lo que tiene React en este punto es un plano actualizado de la interfaz de usuario deseada.
Paso 5: la reconciliación encuentra las diferencias
A continuación, React compara el resultado anterior:
Old Tree
con el nuevo:
New Tree
para determinar qué es lo que realmente ha cambiado. En este ejemplo, antes:
Before:
<h1>0</h1>
y después:
After:
<h1>1</h1>
Solo el texto dentro del encabezado difiere, por lo que ese es el único cambio que registra React. A esta comparación se le conoce como reconciliación. Para conocer más detalles sobre cómo React decide qué mantener y qué reemplazar, consulte cómo crear un modelo mental para la reconciliación, el estado y los Hooks en React.
Paso 6: la fase de confirmación afecta al DOM
Una vez conocida la lista de cambios, React entra en la fase de confirmación y los aplica al DOM real:
DOM Before
<h1>0</h1>
DOM After
<h1>1</h1>
Solo ahora el usuario ve el nuevo número. Este es el momento que la mayoría de las personas imagina cuando llaman a un setter, pero en realidad es el último de varios pasos.
Por qué React no aplica las actualizaciones de inmediato
Considere un manejador que actualiza varios elementos de estado al mismo tiempo:
setCount(c => c + 1);
setLoading(false);
setUser(data);
Si cada llamada desencadenara su propio renderizado, obtendríamos:
Render 1
Render 2
Render 3
Eso son tres renderizos, dos de ellos innecesarios, y posiblemente pantallas intermedias que muestran combinaciones inconsistentes del estado. En cambio, React agrupa las actualizaciones. Las llamadas se ponen en cola:
Update
Update
Update
y luego se procesan juntas:
|
▼Single Render
El procesamiento por lotes evita renders redundantes y garantiza que el usuario siempre vea un estado final consistente. Es la razón principal por la cual React prefiere programar las actualizaciones en lugar de ejecutarlas al instante. React también puede omitir por completo ciertas tareas: si una actualización produce un valor idéntico al actual, según lo determine Object.is, React puede abandonar el proceso sin volver a renderizar los hijos del componente.
Así es en la práctica
Tomemos un cuadro de búsqueda donde cada tecla presionada actualiza tres valores de estado:
query
results
loading
Es natural suponer que cada uno de esos setters provoca un render independiente. Sin embargo, el análisis de rendimiento de tal componente suele mostrar lo contrario: las actualizaciones realizadas juntas en el mismo evento se procesan por lotes, por lo que el componente se renderiza menos veces de lo que sugeriría una lectura superficial del código. La tarea de React no es solo aplicar las actualizaciones, sino hacerlo de manera eficiente.
Cuando varios componentes tienen actualizaciones pendientes
Las actualizaciones no se limitan a un solo componente. Considere un árbol como este:
App
├── Header
├── Sidebar
└── Dashboard
Si varias actualizaciones llegan a diferentes partes de este árbol, React no necesita reconstruir todo de forma indiscriminada. El árbol Fiber permite a React rastrear:
- qué componentes tienen tareas pendientes
- dónde en el árbol se originó cada actualización
- qué subárboles necesitan ser visitados y cuáles se pueden omitir
Esto es lo que permite a React ser mucho más selectivo que un enfoque de “volver a renderizar toda la aplicación con cada cambio”. Tenga en cuenta que un componente que vuelve a renderizarse sigue re-renderizando sus hijos por defecto; la memorización es lo que permite omitir los subárboles que no han cambiado.
Volviendo al ejemplo de console.log obsoleto
De vuelta al fragmento inicial:
setCount(count + 1);
console.log(count);
La consola muestra:
0
Porque el código sigue ejecutándose dentro de la renderización actual. La cola aún no ha sido procesada, no ha tenido lugar la siguiente renderización y la actualización está esperando su turno. Una forma más precisa de entenderlo:
Current Render
count = 0
Request Update
Future Render
count = 1
Si necesita el nuevo valor de inmediato, calcúlelo en una variable local y úsela, o léalo en la siguiente renderización o en un efecto que dependa de él. Visto de esta manera, el comportamiento no resulta en absoluto extraño.
Cómo se conectan las partes
Juntas, las etapas forman una sola cadena:
- El estado se almacena junto con los datos del Hook de un componente.
- Los datos del Hook se adjuntan a la Fiber del componente.
- Llamar a un setter crea una actualización.
- Las actualizaciones se añaden a la cola del Hook.
- El programador de tareas elige el momento adecuado para procesarlas.
- La fase de renderización llama nuevamente al componente y aplica la cola.
Los conceptos que a menudo se enseñan por separado son en realidad pasos consecutivos dentro de un mismo proceso. En una sola frase: una llamada al setter deja el estado tal como está y, en su lugar, solicita una actualización programada, que React aplica en un render posterior, compara con la salida anterior y confirma cambios en el DOM solo donde hay diferencias.
Puntos clave
- Un setter nunca modifica el estado directamente; en su lugar, agrega una actualización para que React la maneje más tarde.
- Las actualizaciones se agrupan por Hook, de modo que varias pueden procesarse en un único render.
- Los valores simples reemplazan el estado; las funciones de actualización derivan el siguiente estado a partir del más reciente, lo que evita valores obsoletos.
- React programa el trabajo en lugar de renderizar en cada llamada, lo que hace posible el agrupamiento y la priorización.
- El renderizado y la actualización del DOM son etapas separadas: el renderizado genera una descripción, mientras que la fase de confirmación modifica el navegador.
- La cola de actualizaciones se encuentra junto con el estado del Hook en la Fiber del componente.
Tratar setState como una solicitud en lugar de como una orden es un pequeño cambio en la redacción con consecuencias importantes. Es la razón por la cual son posibles el agrupamiento, la programación, la reconciliación y el renderizado concurrente. La siguiente pregunta lógica es cómo decide React si las actualizaciones en cola generan un único renderizado o varios, algo que aborda el agrupamiento automático introducido en React 18.
Lecturas relacionadas
- Patrones de Diseño React: Desde el OOP Clásico hasta los Gancho Modernos — Explica cómo se aplican en React patrones de software clásicos como Singleton, Factory y Observer, junto con patrones específicos de React como HOCs, Hooks y Componentes Compuestos.
- Comprendiendo las Lanes de React: Cómo las Máscaras Binarias Codifican la Prioridad de Actualización — Aprenda cómo React codifica múltiples prioridades de actualización en un único número entero mediante máscaras binarias, y por qué las operaciones bit a bit reemplazan a las simples banderas booleanas para la programación de tareas.