Módulo 3: State With Usestate
Presentación del módulo: la interfaz que cambia sola
Por qué este módulo existe aquí
Los dos primeros módulos te enseñaron a describir una interfaz fija. En el módulo 1 clavaste la tesis de la guía: un componente es una función que recibe props y devuelve UI, UI = f(props). En el módulo 2 aprendiste a escribir esa UI con JSX —las llaves con JavaScript adentro, el renderizado condicional, las listas con key—. Con eso puedes construir cualquier pantalla estática que imagines. Pero, si lo piensas, en todo ese recorrido hubo un techo que no podías romper: la pantalla no cambiaba sola. El único modo de que la UI se viera distinta era que tú, desde afuera, llamaras a la función con props distintas. El usuario que escribe en la barra de búsqueda, el clic que expande una tarjeta, el botón que suma un producto al carrito: nada de eso podía ocurrir todavía, porque un componente sin estado es una foto fija. Le das datos, te devuelve una imagen, y ahí se queda.
Este módulo rompe ese techo. Le vamos a dar a los componentes una memoria propia: datos que le pertenecen, que persisten entre renders, y que —cuando cambian— hacen que React vuelva a ejecutar el componente y actualice la pantalla. Esa memoria se llama estado, y con ella la ecuación de la guía se completa. Ya no es solo UI = f(props); ahora es:
UI = f(estado)
Léela con el mismo cuidado que la del módulo 1. La UI sigue siendo el resultado de aplicar una función a unos datos; lo nuevo es que esos datos —el estado— pueden cambiar con el tiempo, y cada vez que cambian, React vuelve a aplicar f y obtiene la nueva UI. Fíjate en lo que no cambia: sigues describiendo cómo se ve la interfaz para unos datos, no manipulando la pantalla a mano. El estado no te devuelve al mundo imperativo del getElementById; al contrario, te deja quedarte en el mundo declarativo aun cuando las cosas se muevan. Tú describes "para este estado, la UI se ve así"; cambias el estado; React reconcilia el DOM. La foto fija se convierte en video, pero cada fotograma se sigue describiendo, no esculpiendo.
Ahora, el estado de React tiene fama de confundir a quien empieza, y con razón: se comporta de tres maneras que, la primera vez, parecen contraintuitivas. Este módulo existe para que las tres se te vuelvan obvias, porque una vez las entiendes, el estado deja de sorprenderte para siempre. Son estas:
- El estado es un snapshot. Dentro de un render, la variable de estado es una constante fija —una foto del valor en ese instante—. Por eso, y esto descoloca a todos la primera vez, escribir
setCount(count + 1)dos veces seguidas suma 1, no 2. Lo vas a medir en la lección 4, no a creértelo de palabra. - Cambiar el estado dispara un re-render. No modificas una variable en el sitio como harías con una variable normal de JavaScript. Le pides a React, con el setter, que vuelva a ejecutar el componente con un valor nuevo.
setno es una asignación; es una solicitud de nuevo render. - El estado no se muta. Para cambiar un objeto o un array del estado, creas uno nuevo en vez de modificar el existente. La razón es concreta y la vas a ver ejecutada: React decide si re-renderiza comparando referencias, y una mutación deja la misma referencia, así que pasa inadvertida.
El módulo entero gira alrededor de estas tres ideas, y cada una se mide ejecutando código. Como en toda la guía, nada se cita de memoria. React del navegador no se puede "correr y ver el DOM" dentro de un agente, así que hacemos lo honesto: la sintaxis JSX real con useState se muestra en los bloques —es la API exacta que escribirás— y la lógica del estado se ejecuta en Node, con solo JavaScript. Para lograrlo modelamos useState con una pequeña celda de memoria que persiste entre renders, modelamos la cola de actualizaciones que explica el snapshot y el updater, y modelamos la comparación por referencia que explica la inmutabilidad. Cada salida en los bloques "Qué esperar" es la salida literal de correr el código con Node 18. Podrás copiarla y reproducirla idéntica.
Una analogía: la función que por fin tiene una libreta
Recuerda cómo describimos un componente en el módulo 1: una máquina expendedora. Aprietas un botón (le pasas props), cae un producto (devuelve UI), y la máquina no recuerda nada entre un uso y el siguiente. Cada vez que la usas es como si fuera la primera: no sabe qué apretaste antes, no guarda cuentas, no tiene pasado. Esa amnesia era una virtud en el módulo 1 —la llamamos pureza, y es lo que hace predecible a un componente—. Pero también era el techo: una máquina sin memoria no puede, por ejemplo, llevar la cuenta de cuántos productos van en el carrito, porque para eso hay que recordar el número anterior y sumarle uno.
El estado es darle a esa función una libreta. Imagina que a la máquina expendedora le pegas, a un lado, una pequeña libreta donde puede anotar una cosa y volver a leerla la próxima vez. Ahora, cuando cae un refresco, puede anotar "van 3 vendidos hoy"; la siguiente vez lee "3", suma uno, y anota "4". La libreta persiste entre usos: lo que escribe hoy sigue ahí mañana. Eso es exactamente el estado: una libreta privada del componente, que sobrevive entre renders, donde guarda las cosas que necesita recordar.
Hay dos detalles de la libreta que son, tal cual, dos reglas del estado que verás una y otra vez. Primero: la libreta es privada. Es de esa máquina; ninguna otra la puede leer ni escribir. Así es el estado: pertenece a un componente y solo ese componente lo cambia (distinto de las props, que le llegan prestadas del padre —lo veremos en la lección 2—). Segundo: no tachas la anotación vieja de cualquier manera, sino que sigues un ritual para cambiarla. En React ese ritual es el setter: no escribes en la libreta a mano (count = 4 no funciona), le avisas a React con setCount(4) que quieres cambiar la anotación, y React se encarga de guardarla y de repintar lo que dependa de ella. Ese "y repintar" es la magia que convierte la foto fija en interfaz viva: cambiar la anotación no solo actualiza el papel, dispara que la máquina vuelva a producir su salida con el número nuevo.
Guarda la imagen de la libreta pegada a la máquina; es todo el módulo. Una función que antes no recordaba nada, ahora tiene un papelito propio donde anota lo que cambia, y cada vez que reescribe ese papelito, su salida se vuelve a calcular. UI = f(estado) es eso: la salida es función de lo que dice la libreta, y la libreta puede cambiar.
Lo que ya sabes, y lo que se mueve ahora
Para ubicarte, vale la pena poner lado a lado lo estático (lo que dominaste) y lo dinámico (lo de este módulo).
En los módulos 1 y 2, el storefront de Mercado era una foto. App recibía un catálogo de productos por props y los describía; ProductCard recibía un producto y lo mostraba; SearchBar era una barra de búsqueda decorativa —se veía, pero escribir en ella no hacía nada, porque no había dónde guardar lo escrito ni qué hacer con ello—. Toda variación de la pantalla venía de afuera: si querías ver otra lista, llamabas App con otro catálogo.
En este módulo, tres cosas del storefront cobran memoria propia:
- La
SearchBargana un estadoquery: el texto que el usuario va escribiendo. Ese texto es suyo, vive en su libreta, y cambia cada vez que el usuario teclea. (En la lección 5 del módulo 5 esequeryfiltrará la lista; aquí solo aprendemos a guardarlo y actualizarlo.) - Un
ProductCardgana un toggle —un estado booleanoexpanded— que decide si muestra los detalles largos del producto o solo lo esencial. Darle clic cambia ese booleano, y la tarjeta se redibuja expandida o colapsada. - El carrito gana un contador y, más adelante, una lista de líneas. Cada vez que sumas un producto, el número sube, y la insignia del carrito se repinta.
Fíjate en el patrón común: en cada caso hay una cosa que cambia con el tiempo por acción del usuario —el texto de búsqueda, el estar-expandido, la cuenta del carrito— y esa cosa necesita vivir en algún lado que persista y que, al cambiar, repinte la parte de la pantalla que depende de ella. Ese "algún lado" es el estado. Identificar qué cosa cambia y darle su pieza de estado es, en buena medida, de lo que trata construir una interfaz viva.
Ejemplo trabajado: el contador del carrito, de 0 a 3
Vamos a ver, ejecutada, la mecánica completa —memoria que persiste, setter que la cambia, re-render que repinta— en su forma más simple: un contador. Es el "hola mundo" del estado, y contiene todo lo esencial.
Así se escribe en React de verdad. Esto es JSX con useState, la API real que usarás; obsérvala, que la estudiamos a fondo en la lección 3:
import { useState } from 'react';
function CartBadge() {
const [count, setCount] = useState(0); // la libreta: arranca en 0
return (
<span className="cart-badge">
Cart ({count})
</span>
);
}
Léelo despacio. useState(0) le pide a React una pieza de estado que empieza en 0, y devuelve dos cosas: el valor actual (count) y una función para cambiarlo (setCount). El componente muestra Cart (0), Cart (1), etc., según lo que diga count. Cuando algo llame a setCount(1) —un clic, en el módulo 4—, React guardará el 1 en la libreta y volverá a ejecutar CartBadge, que ahora leerá count valiendo 1 y mostrará Cart (1). Esa es la vida del estado en cuatro palabras: leer, cambiar, re-ejecutar, repintar.
¿Cómo lo ejecutamos si no hay navegador ni React? Con el truco que sostiene el módulo: modelamos useState a mano. Le damos a nuestro "componente" una celda de memoria que persiste entre llamadas y un setter que la cambia y pide un nuevo render. Es React en miniatura, pero la lógica es idéntica:
'use strict';
// ── Mini-runtime de hooks: lo que React hace por dentro, en miniatura ──
// Una celda de memoria que persiste entre renders + un setter que la cambia
// y pide un nuevo render. Es el corazon de useState, modelado a mano.
let cells = [];
let cursor = 0;
let renderApp = () => {};
function useState(initial) {
const i = cursor++;
if (!(i in cells)) cells[i] = initial; // el inicial SOLO en el primer render
const setState = (next) => {
cells[i] = next;
renderApp(); // cambiar el estado DISPARA un nuevo render
};
return [cells[i], setState];
}
// f: la UI es funcion del estado. Aqui, un contador del carrito.
let ui;
function CartBadge() {
const [count, setCount] = useState(0); // el estado propio del componente
console.log(` UI => Cart (${count})`); // asi se "ve" para este estado
return { setCount, count };
}
function render() {
cursor = 0;
ui = CartBadge();
}
renderApp = render;
console.log('=== montaje: primer render (estado inicial 0) ===');
render();
console.log('\n=== el usuario agrega un producto: setCount(1) ===');
ui.setCount(1);
console.log('\n=== agrega otro: setCount(2) ===');
ui.setCount(2);
console.log('\n=== agrega otro mas: setCount(3) ===');
ui.setCount(3);
Qué esperar. Al correr el archivo con Node, la salida es exactamente esta:
=== montaje: primer render (estado inicial 0) ===
UI => Cart (0)
=== el usuario agrega un producto: setCount(1) ===
UI => Cart (1)
=== agrega otro: setCount(2) ===
UI => Cart (2)
=== agrega otro mas: setCount(3) ===
UI => Cart (3)
Esa secuencia es toda la mecánica del estado en cuatro renders. Vamos por partes, porque cada una prefigura una lección del módulo.
El primer render (montaje) leyó la libreta por primera vez. Como nunca se había inicializado, useState(0) puso un 0 en la celda y lo devolvió; la UI se describió como Cart (0). Ese "el inicial solo la primera vez" es el tema de la lección 3.
Luego, setCount(1) hizo dos cosas a la vez, y es crucial que veas las dos. Primero, guardó 1 en la misma celda de memoria (reescribió la libreta). Segundo, volvió a ejecutar el componente (renderApp()), que releyó la celda —ahora valiendo 1— y se describió como Cart (1). Cambiar el estado no solo actualizó un dato: disparó un nuevo render. Ese "cambiar dispara re-render" es el tema de la lección 5, y es el motor de toda interfaz viva.
Y observa la persistencia: cada render leyó el valor que dejó el anterior. El 2 no salió de la nada; salió de que la celda recordaba que ya iba en... bueno, aquí pusimos los valores a mano (setCount(2)), pero la libreta conservó cada anotación entre render y render. En una app real, el clic haría setCount(count + 1), apoyándose en lo que la libreta ya guardaba. Esa memoria que sobrevive es, literalmente, lo que un componente sin estado no tenía.
Una cosa que quiero que notes desde ya, porque es la semilla de la lección 4: en cada render, count fue una constante. Durante la ejecución de CartBadge con count valiendo 1, ese 1 no cambió a media función; era un valor fijo, la foto de ese render. Solo el siguiente render vio un valor distinto. Guárdalo; volveremos a ello con una medición que sorprende.
El mapa del módulo
Estas son las paradas del módulo, en orden, y cómo cada una construye una parte del modelo mental del estado:
Idea Leccion Concepto clave
──────────────────────────────────── ──────── ──────────────────────────────────
Estado vs props L2 propio y cambia (estado) vs prestado
y de solo lectura (props)
El hook useState L3 const [x, setX] = useState(inicial);
devuelve un PAR; inicial solo la 1a vez
El estado es un snapshot L4 dentro de un render, el estado es
constante; setX(x+1) x2 suma 1, no 2
Actualizar dispara re-render L5 UI = f(estado); set agenda un render;
la maquina render -> set -> re-render
El updater funcional L6 setX(x => x+1) para basarse en el
anterior; x2 SI suma 2; batching
No mutes el estado L7 crea objeto/array nuevo; React compara
referencias (Object.is)
──────────────────────────────────── ──────── ──────────────────────────────────
Dale estado al storefront L8 el mini-proyecto, ejecutado
Las lecciones 4, 5, 6 y 7 son, en el fondo, las cuatro caras de una misma idea —que set no es una asignación normal, sino una solicitud de nuevo render sobre una libreta que se compara por referencia—. Si las cuatro te encajan, entendiste el estado de React mejor que la mayoría.
La frontera: qué NO entra en este módulo
Como en cada módulo, saber dónde termina el terreno te ahorra confusión. Tres fronteras importan aquí, y las tres son con módulos de esta misma guía:
- Los eventos que llaman a los setters son el módulo 4. En este módulo aprendemos a declarar estado y a actualizarlo, pero llamamos a los setters con handlers simples o "a mano" en los ejemplos ejecutados. El
onClickque disparasetCount, elonChangede un input controlado que disparasetQuery—el cableado real entre la acción del usuario y el setter— es la lección del módulo 4. Aquí, cuando escribassetQuery('mo'), imagina que un evento lo provocó; el evento en sí lo estudiamos después. - Derivar en vez de guardar es el módulo 5. Una tentación natural al aprender estado es guardar todo en estado: el
query, y además la lista filtrada, y además el total. Pero mucho de eso no es estado: se calcula a partir del estado en cada render. La lección 7 de este módulo te da la regla ("una pieza de estado por cosa que cambia") y el módulo 5 la desarrolla a fondo: por qué el estado derivado guardado se desincroniza y cómo se computa en su lugar. Aquí guardamos solo lo que de verdad cambia por sí mismo. - Levantar el estado a un ancestro común es el módulo 7. Cuando dos componentes necesitan compartir un estado —el carrito que
Appposee pero queProductCardyCartdeben ver—, ese estado "sube" a su ancestro común y baja por props. Eso es levantar el estado, y es el módulo 7. En este módulo el estado vive local a cada componente: elqueryen laSearchBar, elexpandeden suProductCard. Aún no lo compartimos.
Y las fronteras de siempre con las guías hermanas siguen en pie: el estado global (Context a fondo, Zustand, Redux) y los datos del servidor (React Query, caché) son de frontend-state-and-data; aquí llegamos hasta useState local. Los efectos para sincronizar con sistemas externos son el módulo 6 de esta guía, no este.
Errores comunes
Confundir el estado con una variable normal de JavaScript. Qué pasa: alguien intenta cambiar el estado con una asignación directa —count = count + 1 o count++— y se frustra porque "no pasa nada" en la pantalla. Por qué pasa: es lo natural en JavaScript; una variable se cambia asignándole. Cómo detectarlo: modificas la variable de estado sin usar su setter, y la UI no se actualiza (o React ni se entera). Cómo corregirlo: el estado solo se cambia con su setter (setCount(...)), porque el setter hace dos cosas que una asignación no hace: guarda el valor en la libreta que React controla, y dispara el re-render. Una asignación a la variable local no toca la libreta de React y no repinta nada; a lo sumo cambia una copia que se descarta al terminar el render. Piensa en la variable de estado como de solo lectura dentro del render: la lees, pero para cambiarla usas el setter.
Esperar que el componente "recuerde" sin estado. Qué pasa: alguien pone una variable normal dentro del componente (let count = 0) esperando que conserve su valor entre renders, y descubre que siempre vuelve a 0. Por qué pasa: parece que una variable "dentro" del componente debería persistir. Cómo detectarlo: la cuenta se reinicia en cada render; nada se acumula. Cómo corregirlo: una variable local nace y muere con cada llamada al componente —el componente es una función, y sus variables locales son efímeras—. Lo único que persiste entre renders es lo que guardas con useState (la libreta). Si necesitas que algo se recuerde, va en estado; si es un cálculo intermedio que se puede rehacer cada render, va en una variable local.
Querer que la pantalla cambie sin re-render. Qué pasa: alguien cambia el estado (correctamente, con el setter) pero espera que la pantalla se actualice "en el sitio", en la misma línea, y se confunde cuando el valor que lee justo después del setter sigue siendo el viejo. Por qué pasa: uno imagina que setCount(1) es como count = 1, inmediato. Cómo detectarlo: lees count justo después de setCount y no ves el valor nuevo. Cómo corregirlo: interioriza que set agenda un nuevo render, no cambia la variable de esta pasada. El valor nuevo aparece en el siguiente render, no en la línea siguiente. Esto es el snapshot (lección 4) y el motor del re-render (lección 5); por ahora, quédate con que set es una solicitud de repintado, no una asignación instantánea.
Ejercicios
Ejercicio 1 — Estado o no estado. Para cada uno de estos datos del storefront, decide si debe ser estado (algo que cambia con el tiempo por acción del usuario y necesita recordarse) o no (algo fijo, o algo que llega por props, o algo que se puede calcular): (a) el texto que el usuario escribe en la SearchBar; (b) el nombre de un producto en su ProductCard; (c) si un ProductCard está expandido o colapsado; (d) el precio formateado que se muestra ("$25.99"), dado que el precio en centavos llega por props.
Ver solución
- (a) El texto de búsqueda: SÍ es estado. Cambia con el tiempo, por acción del usuario (teclea), y el componente necesita recordarlo entre renders para mostrarlo y usarlo. Vive en la libreta de la
SearchBarcomoquery. - (b) El nombre de un producto: NO es estado. Llega por props (
props.name), desde el catálogo que poseeApp. Es un dato prestado, de solo lectura desde elProductCard; no cambia por acción del usuario dentro de la tarjeta. Es props, no estado (la distinción exacta es la lección 2). - (c) Si está expandido: SÍ es estado. Es una cosa que cambia con el tiempo (el usuario da clic para expandir/colapsar), es propia de esa tarjeta, y hay que recordarla. Vive como un booleano
expandeden el estado delProductCard. - (d) El precio formateado: NO es estado. Se calcula a partir de una prop (
formatPrice(props.priceCents)) en cada render. No cambia por sí solo ni hay que recordarlo; se recomputa cuando hace falta. Guardarlo en estado sería un error de estado derivado (la razón a fondo es el módulo 5).
La pregunta clave para cada dato: ¿cambia por sí mismo con el tiempo y hay que recordarlo? Si sí, es estado. Si llega de afuera (props) o se puede recalcular (derivado), no.
Ejercicio 2 — Traza los renders. Sin correr nada, predice la salida (las líneas UI => Cart (...)) del ejemplo trabajado si la secuencia de llamadas fuera esta en vez de la original:
render(); // montaje
ui.setCount(5);
ui.setCount(2);
Ver solución
UI => Cart (0)
UI => Cart (5)
UI => Cart (2)
El montaje leyó la celda recién inicializada en 0, así que la UI fue Cart (0). Luego setCount(5) guardó 5 en la celda y disparó un render, que leyó 5 → Cart (5). Luego setCount(2) guardó 2 (reemplazando el 5) y disparó otro render, que leyó 2 → Cart (2). Cada setter reemplaza lo que había en la libreta con el valor que le pasas, y dispara un render que lee el nuevo valor. Nota que setCount(2) no "resta 3": simplemente pone 2, sin importar que antes hubiera 5.
Ejercicio 3 — Explica la ecuación. Un compañero que viene del módulo 1 dice: "yo ya sabía que UI = f(props). ¿Qué añade UI = f(estado)? ¿No es lo mismo con otro nombre?". Explícale con tus palabras qué cambia de fondo entre las dos, usando el ejemplo del contador.
Ver solución
La forma de la ecuación es la misma —la UI es el resultado de aplicar una función a unos datos—, pero cambia de fondo de dónde vienen los datos y quién puede cambiarlos, y eso cambia todo.
En UI = f(props), los datos (las props) vienen de afuera: los pasa el padre, y el componente no puede cambiarlos. Para que la UI cambie, alguien externo tiene que llamar a la función con props distintas. El componente es pasivo: recibe y describe. Por eso la pantalla no podía cambiar sola.
En UI = f(estado), los datos (el estado) viven dentro del componente, en su libreta, y el componente puede cambiarlos él mismo con su setter. Y —esta es la clave— cuando los cambia, React vuelve a ejecutar el componente solo, sin que nadie de afuera lo vuelva a llamar. En el contador: CartBadge guarda count en su estado; cuando setCount(1) corre, React re-ejecuta CartBadge por su cuenta y la UI pasa de Cart (0) a Cart (1). Nadie de afuera tuvo que re-llamar la función con props nuevas.
Ese es el salto: de una función pasiva que depende de que la alimenten desde afuera, a una función con memoria propia que puede cambiar sus datos y provocar su propia actualización. Eso es lo que convierte una foto fija en una interfaz viva.
Resumen y siguiente paso
En esta lección instalaste el salto que da vida a la guía: de UI = f(props) —la pantalla estática de los módulos 1 y 2— a UI = f(estado) —la interfaz que cambia sola—. Entendiste que el estado es la memoria propia de un componente (la libreta pegada a la máquina): datos que le pertenecen, que persisten entre renders, y que —al cambiar con su setter— disparan un re-render que repinta la parte de la pantalla que depende de ellos. Lo mediste ejecutando el contador del carrito de 0 a 3, viendo la mecánica completa: la celda que se inicializa una vez, el setter que la reescribe y re-ejecuta el componente, y la memoria que persiste entre renders. Y guardaste el mapa de las tres ideas que gobiernan el módulo: el estado es un snapshot, cambiarlo dispara re-render, y no se muta.
Antes de avanzar deberías poder: explicar qué es el estado y en qué se distingue de una variable normal (persiste; se cambia con el setter, no con =); decidir si un dato debe ser estado (¿cambia solo con el tiempo y hay que recordarlo?); trazar la secuencia de renders de un contador dado sus setters; y ubicar las fronteras (eventos en M4, derivar en M5, levantar estado en M7).
La lección 2 afila la distinción más importante de arranque: estado vs props. Vas a ver, ejecutado, por qué las props son datos prestados (vienen del padre, de solo lectura) y el estado es dato propio (vive en el componente, cambia con su setter), y a decidir con criterio cuál usar para cada cosa del storefront. Es la brújula que evita el error más común del principiante: guardar en estado lo que debía ser prop, o tratar como fija una prop que debía ser estado.
Recursos
- React, "State: A Component's Memory" — react.dev/learn/state-a-components-memory. La página oficial que introduce el estado como la memoria de un componente y presenta
useState. El mejor punto de partida para este módulo. En inglés. - React, "Adding Interactivity" — react.dev/learn/adding-interactivity. El índice de la sección que cubre estado, eventos y renderizado; todo lo de este módulo (y el 4) vive aquí. En inglés.
- React, "Render and Commit" — react.dev/learn/render-and-commit. Explica que actualizar el estado dispara que React vuelva a ejecutar el componente; la base del "set dispara re-render". En inglés.
- React, "Reacting to Input with State" — react.dev/learn/reacting-to-input-with-state. Cómo pensar una UI declarativa en términos de estados, en lugar de manipular el DOM paso a paso. En inglés.