Módulo 3: State With Usestate
Actualizar el estado dispara un re-render
Descripción
La lección anterior mostró qué pasa dentro de un render: el estado es un snapshot, una foto fija. Esta lección mira la otra mitad: qué pasa después de que llamas al setter. Y lo que pasa es el motor de toda interfaz viva: actualizar el estado dispara un re-render. Cambiar el estado no toca la pantalla directamente; le pide a React que vuelva a ejecutar el componente con el estado nuevo, produzca una UI nueva, y reconcilie el DOM para que coincida. Es UI = f(estado) puesto en movimiento: cambia el estado (la entrada), React re-ejecuta f, sale una UI nueva (la salida).
Este es el mecanismo que echabas de menos en los módulos 1 y 2. Allí, la única forma de ver otra pantalla era que tú llamaras al componente con props distintas. Aquí, el componente se re-ejecuta solo cuando su estado cambia. Nadie de afuera lo vuelve a llamar; React lo hace por su cuenta, porque el setter se lo pidió. Ese "se actualiza solo en respuesta a un cambio de estado" es, literalmente, lo que convierte una descripción estática en una interfaz que reacciona.
Vale la pena nombrar el ciclo completo, porque React lo tiene bien definido. Cuando llamas a un setter, React (1) agenda un re-render (encola el cambio, como viste en la lección 4); (2) renderiza, es decir, vuelve a ejecutar tu componente con el estado nuevo, obteniendo la nueva descripción de UI; y (3) hace commit, o sea, aplica al DOM real solo las diferencias entre la descripción vieja y la nueva. En este módulo nos centramos en los pasos 1 y 2 —agendar y re-ejecutar—, que son los que puedes modelar y medir; el paso 3 (cómo React calcula y aplica las diferencias mínimas al DOM, la reconciliación) es maquinaria interna que no necesitas manipular.
Conexión con el módulo. Snapshot (lección 4) y re-render (esta) son las dos caras de cómo se comporta el estado al cambiarlo: el snapshot explica que la variable de este render no cambia; el re-render explica que el valor nuevo aparece en el siguiente, porque el componente se vuelve a ejecutar. Juntas cierran el "qué pasa cuando llamo al setter". Y esta lección da el marco para la 6 (el updater, que afina qué valor llega al re-render) y la 7 (la inmutabilidad, que decide si React re-renderiza según la referencia). Aquí instalamos la idea central: set = solicitud de nuevo render.
Una analogía: el pizarrón que se reescribe entero
Imagina un pizarrón donde un profesor lleva la cuenta de un partido: "Local 2 — Visitante 1". Cuando el visitante anota, el profesor no va con un borrador diminuto a raspar solo el 1 y encimarle un 2 en el mismo trazo. Hace algo más limpio: borra el pizarrón entero y lo reescribe completo con el marcador nuevo, "Local 2 — Visitante 2". El resultado en el pizarrón es idéntico a si hubiera parchado solo el número, pero el método es "rehacer todo desde los datos actuales", no "modificar el pedacito que cambió".
Así funciona el re-render. Cuando el estado cambia, React no busca "el pedacito de pantalla afectado" para parcharlo a mano (eso era el mundo imperativo del getElementById del módulo 1). React vuelve a ejecutar tu componente entero con el estado nuevo, y obtiene una descripción completa y fresca de cómo se ve la UI para ese estado. Tú describes el pizarrón completo ("Local X — Visitante Y") en función de los datos; React se encarga de mostrarlo. No piensas en transiciones ("¿qué número cambió?"); piensas en estados ("¿cómo se ve el marcador para estos datos?").
Ahora, un matiz importante para no asustarse: que React conceptualmente "reescriba el pizarrón entero" (re-ejecute tu componente) no significa que repinte todo el DOM del navegador. Esa es la parte inteligente: React compara el pizarrón nuevo con el viejo y solo aplica al DOM las diferencias reales —si solo cambió el 1 por el 2, solo ese texto se actualiza en pantalla—. Tú razonas como si se reescribiera todo (simple: UI = f(estado)), y React optimiza por debajo (rápido: solo toca lo que cambió). Guarda el pizarrón que se reescribe entero: es cómo piensas el re-render, aunque React sea astuto al aplicarlo.
Ejemplo trabajado: la máquina de estado de un toggle, render por render
Vamos a ver el ciclo render → set → re-render ejecutado, trazando la secuencia de renders. Modelamos un ProductCard con un toggle expanded (mostrar u ocultar los detalles largos del producto), y hacemos que cada cambio de estado dispare un nuevo render, imprimiendo qué UI produce cada uno.
Así se escribe en React de verdad:
import { useState } from 'react';
function ProductCard() {
const [expanded, setExpanded] = useState(false);
const view = expanded
? 'Wireless Mouse | $25.99 | Ergonomic, 2.4GHz, 18-month battery'
: 'Wireless Mouse | $25.99';
return (
<article className="product-card">
<p>{view}</p>
{/* el boton que llama a setExpanded(!expanded) es el modulo 4 */}
</article>
);
}
La UI es función del estado: si expanded es false, muestra lo esencial; si es true, muestra los detalles. Cambiar expanded con su setter dispara un re-render que recalcula view. Vamos a ejecutar esa máquina. Usamos el mini-runtime de hooks (la celda que persiste), y cada setter llama a renderApp() para re-ejecutar el componente:
'use strict';
// ── UI = f(estado): cambiar el estado dispara un re-render ──
// Cada setState vuelve a ejecutar el componente y produce una UI NUEVA
// a partir del estado nuevo. Modelamos la maquina render -> set -> re-render.
let cells = [];
let cursor = 0;
let renderApp = () => {};
let renderCount = 0;
function useState(initial) {
const i = cursor++;
if (!(i in cells)) cells[i] = initial;
const setState = (next) => {
cells[i] = next;
renderApp(); // el estado cambio: re-render
};
return [cells[i], setState];
}
// f: la UI de un ProductCard con un toggle "ver detalles".
let ui;
function ProductCard() {
const [expanded, setExpanded] = useState(false);
const view = expanded
? 'Wireless Mouse | $25.99 | Ergonomic, 2.4GHz, 18-month battery'
: 'Wireless Mouse | $25.99';
renderCount++;
console.log(`render #${renderCount}: expanded = ${expanded}`);
console.log(` UI => ${view}`);
return { setExpanded };
}
function render() {
cursor = 0;
ui = ProductCard();
}
renderApp = render;
console.log('=== montaje: primer render ===');
render();
console.log('\n=== el usuario expande: setExpanded(true) ===');
ui.setExpanded(true);
console.log('\n=== el usuario colapsa: setExpanded(false) ===');
ui.setExpanded(false);
console.log(`\ntotal de renders ejecutados: ${renderCount}`);
Qué esperar. Al correr el archivo, la salida es exactamente esta:
=== montaje: primer render ===
render #1: expanded = false
UI => Wireless Mouse | $25.99
=== el usuario expande: setExpanded(true) ===
render #2: expanded = true
UI => Wireless Mouse | $25.99 | Ergonomic, 2.4GHz, 18-month battery
=== el usuario colapsa: setExpanded(false) ===
render #3: expanded = false
UI => Wireless Mouse | $25.99
total de renders ejecutados: 3
Esta salida es la máquina de estado en movimiento. Léela como una secuencia.
Render #1 (montaje). React ejecutó ProductCard por primera vez. expanded valía false (el inicial), así que la UI fue lo esencial: Wireless Mouse | $25.99. Hasta aquí, la foto fija de los módulos 1 y 2.
El usuario expande → render #2. Al llamar setExpanded(true), pasaron las dos cosas que definen el re-render: React guardó true en la celda y volvió a ejecutar ProductCard (por eso hay un render #2). En esa nueva pasada, expanded valía true, y como la UI es función del estado, view se recalculó a la versión con detalles. Nadie reescribió la pantalla a mano: el componente se re-ejecutó entero y produjo una descripción nueva. El pizarrón se reescribió completo con el marcador nuevo.
El usuario colapsa → render #3. setExpanded(false) repitió el ciclo: guardó false, re-ejecutó, y la UI volvió a lo esencial. Cada cambio de estado, un render nuevo; cada render, una UI recalculada desde cero a partir del estado de ese momento.
Y el conteo final: 3 renders. Uno de montaje y dos disparados por los setters. Esa es la equivalencia exacta que quiero que te lleves: cada llamada efectiva al setter = un re-render. La cantidad de renders no es aleatoria; es "el montaje más un render por cada cambio de estado". Es la máquina que hace que la interfaz reaccione.
La máquina de estado, dibujada
El toggle es una máquina de estado de dos posiciones, y cada arista es un setExpanded:
stateDiagram-v2
[*] --> Collapsed: montaje (expanded = false)
Collapsed --> Expanded: setExpanded(true) -> re-render
Expanded --> Collapsed: setExpanded(false) -> re-render
Collapsed: Collapsed<br/>UI: nombre + precio
Expanded: Expanded<br/>UI: nombre + precio + detalles
Cada estado tiene una UI asociada (UI = f(estado)), y cada transición es un setter que dispara un re-render. Pensar tu componente como una máquina de estado —"¿qué estados hay? ¿qué UI corresponde a cada uno? ¿qué transición lleva de uno a otro?"— es una de las formas más limpias de diseñar interfaz en React.
Qué re-renderiza y qué no
Una precisión que evita sustos y malentendidos: cuando el estado de un componente cambia, React re-ejecuta ese componente y sus hijos (para recalcular la descripción del subárbol). No re-ejecuta toda la app: el padre no se re-renderiza porque un hijo cambie su estado. Y —la parte optimizada— aunque React re-ejecute tu componente para obtener la nueva descripción, al DOM real solo aplica las diferencias: si entre el render #1 y el #2 solo cambió un texto, solo ese texto se actualiza en pantalla, no toda la tarjeta. Por eso "re-render" (re-ejecutar tu función para obtener la descripción) no es lo mismo que "repintar todo el DOM" (aplicar cambios, que React minimiza). Tú piensas en describir el estado completo; React se encarga de que aplicarlo sea barato.
Errores comunes
Intentar actualizar el DOM a mano en vez de cambiar el estado. Qué pasa: dentro de un componente se hace document.querySelector('.details').style.display = 'block' para "mostrar los detalles", en lugar de cambiar expanded. Por qué pasa: es la costumbre imperativa de los módulos previos a React. Cómo detectarlo: tu componente toca el DOM directamente para reflejar un cambio, en vez de derivar la UI del estado. Cómo corregirlo: no manipules la pantalla; cambia el estado y deja que el re-render la actualice. En vez de mostrar/ocultar el nodo a mano, describe la UI en función de expanded ({expanded && <Details />}) y llama setExpanded(true). React re-ejecuta y hace coincidir el DOM. Manipular el DOM a mano y tener estado es pedir conflictos: React repintará según el estado y borrará tus cambios manuales.
Esperar que la UI cambie sin llamar al setter. Qué pasa: se muta la variable de estado directamente (expanded = true) o se cambia una variable normal, y la pantalla no reacciona. Por qué pasa: se olvida que solo el setter dispara el re-render. Cómo detectarlo: cambias un dato y la UI no se actualiza, aunque "el valor ya es otro". Cómo corregirlo: solo setExpanded(...) (u otro setter de estado) provoca un re-render. Cambiar una variable local o mutar el estado no avisa a React; la pantalla se queda con la última UI renderizada. Si quieres que la UI cambie, el cambio tiene que pasar por un setter.
Confundir "re-render" con "se repinta todo el DOM y es lento". Qué pasa: alguien evita el estado o hace optimizaciones prematuras por miedo a que "cada render repinte toda la página". Por qué pasa: "re-render" suena a "redibujar todo". Cómo detectarlo: te preocupas por el costo de renders antes de tener un problema medible. Cómo corregirlo: entiende la diferencia entre re-ejecutar tu componente (barato: es correr una función que devuelve una descripción) y actualizar el DOM (donde React aplica solo las diferencias mínimas). Un re-render no repinta la página; recalcula una descripción y hace commit de los cambios reales. En la enorme mayoría de los casos esto es rápido y no requiere que hagas nada. (La optimización de renders —memo, etc.— es de la guía de performance, y solo cuando midas que hace falta.)
Ejercicios
Ejercicio 1 — Cuenta los renders. Para el ProductCard del ejemplo, ¿cuántos renders (incluido el montaje) produce esta secuencia, y cuál es la UI del último? Predícelo sin correr:
render(); // montaje
ui.setExpanded(true);
ui.setExpanded(true);
ui.setExpanded(false);
Ver solución
4 renders. Uno de montaje más uno por cada llamada al setter. Aunque la segunda setExpanded(true) fija el mismo valor que ya tenía (true), en nuestro modelo cada llamada dispara renderApp(), así que cuenta como un render. (Nota: React de verdad puede saltarse el re-render si el valor nuevo es idéntico al actual —esto conecta con la lección 7, donde React compara por referencia—; nuestro modelo simplificado siempre re-renderiza.)
La UI del último render (render #4, expanded = false): Wireless Mouse | $25.99 (lo esencial, sin detalles), porque el estado terminó en false.
La lógica: la secuencia de estados fue false (montaje) → true → true → false, y la UI de cada render es función de ese estado. El último estado manda.
Ejercicio 2 — De imperativo a declarativo. Un compañero, viniendo de jQuery, escribe esto para expandir la tarjeta al hacer clic:
// "cuando den clic, muestro los detalles a mano"
document.querySelector('.card-details').style.display = 'block';
Explica por qué en React no se hace así, y reescríbelo con estado (describe el JSX y el setter, sin preocuparte por el evento en sí, que es el módulo 4).
Ver solución
Por qué no se hace así: manipular el DOM a mano rompe el modelo UI = f(estado). React es el dueño del DOM: en el próximo re-render, va a hacer coincidir la pantalla con lo que tu componente describe, y si tu descripción no incluye "detalles visibles", React los volverá a ocultar, borrando tu cambio manual. Además, tú tendrías que acordarte de mostrar y ocultar a mano en cada caso, que es exactamente el nido imperativo que React vino a eliminar.
Reescrito con estado: el "mostrar/ocultar" se convierte en una pieza de estado, y la UI se describe en función de ella:
function ProductCard(props) {
const [expanded, setExpanded] = useState(false);
return (
<article className="product-card">
<h3>{props.name}</h3>
{expanded && <p className="card-details">{props.details}</p>}
{/* al dar clic: setExpanded(true) — el evento es el modulo 4 */}
</article>
);
}
Ahora no tocas el DOM: describes "si expanded, muestra los detalles" con {expanded && ...}, y para mostrarlos llamas setExpanded(true). React re-renderiza y hace aparecer los detalles. Piensas en el estado ("¿está expandido?"), no en la transición ("busca el nodo y cámbiale el display"). Esa es la traducción de imperativo a declarativo.
Ejercicio 3 — Diseña la máquina de estado. El botón de un producto debe tener tres estados visuales: idle ("Add to cart"), adding ("Adding...") y added ("Added!"). Describe la máquina de estado: qué pieza de estado usarías, qué UI corresponde a cada valor, y qué transición (qué setter) lleva de un estado al siguiente. No implementes los eventos (módulo 4); solo diseña la máquina.
Ver solución
La pieza de estado: un solo estado que guarde en cuál de las tres fases está el botón, por ejemplo const [status, setStatus] = useState('idle'), donde status puede ser 'idle', 'adding' o 'added'. (Una sola pieza de estado con tres valores posibles, no tres booleanos sueltos: eso evita estados imposibles como "adding y added a la vez" —conecta con "una pieza de estado por cosa que cambia", lección 7.)
La UI de cada estado (UI = f(status)):
status === 'idle'→ botón que dice "Add to cart", habilitado.status === 'adding'→ botón que dice "Adding...", deshabilitado.status === 'added'→ botón que dice "Added!", quizás con un check.
Las transiciones (cada una es un setter que dispara re-render):
idle → adding: al dar clic,setStatus('adding').adding → added: cuando la operación termina,setStatus('added').added → idle: tras un momento (o al reiniciar),setStatus('idle').
Dibujada:
idle --clic--> adding --(termina)--> added --(reset)--> idle
Cada estado tiene su UI; cada flecha es un setStatus(...) que re-renderiza el botón con la nueva descripción. Pensar así —estados y transiciones— es diseñar la interacción antes de escribir un solo evento.
Resumen y siguiente paso
En esta lección instalaste el motor de la interfaz viva: actualizar el estado dispara un re-render. Cambiar el estado no toca la pantalla a mano; le pide a React que vuelva a ejecutar el componente con el estado nuevo y produzca una UI nueva (UI = f(estado)). Lo mediste ejecutando la máquina de un toggle: cada setExpanded disparó un render (render #1 montaje, #2 al expandir, #3 al colapsar), y la UI de cada render se recalculó desde el estado de ese momento —3 renders para 1 montaje + 2 cambios—. Entendiste el ciclo (agendar → renderizar → commit), la analogía del pizarrón que se reescribe entero, y la distinción clave: re-ejecutar tu componente (barato, conceptual) no es repintar todo el DOM (React aplica solo las diferencias).
Antes de avanzar deberías poder: explicar que set = solicitud de re-render; trazar la secuencia de renders de una máquina de estado y contar cuántos hay; traducir un cambio imperativo del DOM a "cambia el estado y deja que React repinte"; y distinguir re-ejecutar el componente de actualizar el DOM.
La lección 6 junta el snapshot (lección 4) con el re-render (esta) para resolver un problema concreto: ¿qué haces cuando el nuevo estado depende del anterior y el snapshot te lo impide? La respuesta es el updater funcional setX(x => x + 1). Vas a medir por qué dos updaters seguidos sí suman 2 (a diferencia de dos setX(x + 1)), a entender el batching (cómo React agrupa varios sets de un evento en un solo re-render), y a saber cuándo usar cada forma.
Recursos
- React, "Render and Commit" — react.dev/learn/render-and-commit. La página oficial de esta lección: el ciclo de tres pasos (trigger, render, commit) y por qué actualizar el estado lo dispara. En inglés.
- React, "State as a Snapshot" — react.dev/learn/state-as-a-snapshot. La otra mitad: cada re-render tiene su propia foto del estado; explica qué valor ve el componente re-ejecutado. En inglés.
- React, "Reacting to Input with State" — react.dev/learn/reacting-to-input-with-state. Cómo modelar una UI como una máquina de estados y sus transiciones, en lugar de manipular el DOM paso a paso. La base del enfoque declarativo de esta lección. En inglés.
- React, "You Might Not Need an Effect" — react.dev/learn/you-might-not-need-an-effect. Refuerza que la UI se deriva del estado en el render, no se sincroniza a mano; útil para no caer en manipular el DOM. En inglés.