Módulo 1: The Four Kinds Of State
Estado local: la UI de un componente
Descripción
Empezamos por la caja más simple y más segura de todas: el estado local. Es el estado que un solo componente usa para manejar su propia UI, y que muere cuando ese componente desaparece. Un menú desplegable que está abierto o cerrado (menuOpen), el texto que alguien escribe a medias en un campo antes de enviarlo, si una sección está expandida o colapsada, si el mouse está encima de una tarjeta. Todo eso es privado del componente: nadie más lo necesita, y no tiene sentido que sobreviva a la vida del componente. Es la caja donde vive la mayoría del estado de una app bien clasificada —y, justamente por eso, la caja que la gente vacía de más cuando descubre las herramientas globales y quiere meter todo ahí—.
Conexión con el módulo. En la lección 1 dibujaste las cuatro gavetas y la pregunta que las abre: "¿de quién es la verdad?". El estado local es la respuesta más humilde a esa pregunta: la verdad es de un solo componente, y muere con él. Es la caja a la que llega una pieza cuando las otras tres preguntas —¿es del backend?, ¿es compartible/recargable?, ¿la usan varios componentes lejanos?— dieron todas "no". Por eso es la última rama del árbol de decisión: lo local es lo que sobra después de descartar server, url y global. La herramienta ya la conoces de react-fundamentals —es useState—, así que aquí no la re-enseñamos; lo que trabajamos es reconocer cuándo una pieza pertenece de verdad a esta caja, y qué propiedad la define: el aislamiento por instancia.
Una analogía: lo tuyo, en tu cuarto
Vuelve a la casa de la lección 1. El estado local es tu cepillo de dientes en tu cuarto: una cosa que solo tú usas y que no sale de ahí. Dos detalles de esa imagen importan.
Primero, el aislamiento: si tu hermana también tiene su cepillo en su cuarto, son dos cepillos distintos. Que tú uses el tuyo no mueve el de ella. No hay "un cepillo compartido de la casa"; hay uno por cuarto, cada uno con su propia vida. En React es igual: si diez tarjetas de producto tienen cada una un menuOpen, son diez menuOpen independientes. Abrir el menú de una no abre el de las otras nueve.
Segundo, la mortalidad: cuando te mudas y desarmas tu cuarto, tu cepillo se va contigo o a la basura; no queda flotando en la casa. En React, cuando un componente se desmonta (desaparece de la pantalla), su estado local desaparece con él. Si vuelves a montar el componente después, arranca de cero, con su valor inicial. Esa mortalidad es una característica, no un defecto: el estado local no ensucia el resto de la app con datos viejos, porque se limpia solo.
Guarda las dos ideas —aislado por instancia y muere con el componente—, porque son exactamente lo que vamos a medir.
El caso en Mercado: menuOpen en una ProductCard
En el storefront, cada ProductCard tiene un pequeño menú de acciones ("Agregar a favoritos", "Compartir", "Ver detalles") que se abre al tocar un botón "···". Si ese menú está abierto o no es la pieza de estado local por excelencia: la usa solo esa tarjeta, y a nadie más de la app le importa. Así se ve el componente en React real —esto es lo que escribirás, y es useState puro de react-fundamentals—:
import { useState } from 'react';
function ProductCard({ product }) {
const [menuOpen, setMenuOpen] = useState(false); // estado LOCAL de ESTA tarjeta
return (
<div className="product-card">
<h3>{product.name}</h3>
<p>{formatPrice(product.priceCents)}</p>
<button onClick={() => setMenuOpen((open) => !open)}>···</button>
{menuOpen && (
<ul className="card-menu">
<li>Agregar a favoritos</li>
<li>Compartir</li>
<li>Ver detalles</li>
</ul>
)}
</div>
);
}
Aplícale la regla de decisión, pregunta por pregunta: ¿la verdad de menuOpen vive en el backend? No —al servidor le da igual si abriste un menú—. ¿Querrías compartir por link "el menú de la tarjeta 3 está abierto"? No —nadie manda eso—. ¿Lo necesitan varios componentes lejanos? No —solo esta tarjeta lo lee, para mostrar u ocultar su <ul>—. Las tres primeras preguntas dan "no", así que cae en la última caja: local. Y como cada ProductCard tiene su propia llamada a useState, cada tarjeta tiene su propio menuOpen. Eso —el aislamiento por instancia— es lo que vamos a ver ejecutado.
Ejemplo trabajado: dos menuOpen que no se enteran uno del otro
React no corre en un agente, así que modelamos la mecánica del estado local en Node: una función que fabrica una "tarjeta" con su propia celda de menuOpen, encerrada en un closure para que nadie de afuera la vea —es, en miniatura, lo que hace useState por instancia—. Montamos dos tarjetas, tocamos el menú de una y la otra, y al final desmontamos una para ver cómo su estado local desaparece:
// Estado LOCAL: cada instancia de un componente tiene SU propia celda de estado.
// Modelamos dos ProductCard, cada uno con su menuOpen aislado.
// (Es una mini-version del useState por-instancia de React.)
function makeProductCard(name) {
// Cada tarjeta cierra sobre su propia celda: nadie mas la ve.
let menuOpen = false; // estado LOCAL de ESTA tarjeta
return {
name,
toggleMenu() { menuOpen = !menuOpen; },
render() {
if (menuOpen === undefined) return `${name}: (desmontada, su menuOpen ya no existe)`;
return `${name}: menu ${menuOpen ? 'ABIERTO' : 'cerrado'}`;
},
unmount() { menuOpen = undefined; }, // al desmontar, el estado local desaparece
};
}
console.log('=== Estado local: aislado por instancia ===\n');
const mouseCard = makeProductCard('Wireless Mouse');
const keyboardCard = makeProductCard('Mechanical Keyboard');
console.log('1) Ambas tarjetas recien montadas:');
console.log(' ' + mouseCard.render());
console.log(' ' + keyboardCard.render());
console.log('\n2) Abro el menu SOLO de la tarjeta del mouse:');
mouseCard.toggleMenu();
console.log(' ' + mouseCard.render());
console.log(' ' + keyboardCard.render() + ' <- no se entero: su menuOpen es otro');
console.log('\n3) Abro tambien el del teclado, y cierro el del mouse:');
keyboardCard.toggleMenu();
mouseCard.toggleMenu();
console.log(' ' + mouseCard.render());
console.log(' ' + keyboardCard.render());
console.log('\n4) Desmonto la tarjeta del mouse (el usuario sale de esa vista):');
mouseCard.unmount();
console.log(' ' + mouseCard.render());
console.log(' ' + keyboardCard.render() + ' <- intacta');
Qué esperar. Al correr el archivo con Node, la salida es exactamente esta:
=== Estado local: aislado por instancia ===
1) Ambas tarjetas recien montadas:
Wireless Mouse: menu cerrado
Mechanical Keyboard: menu cerrado
2) Abro el menu SOLO de la tarjeta del mouse:
Wireless Mouse: menu ABIERTO
Mechanical Keyboard: menu cerrado <- no se entero: su menuOpen es otro
3) Abro tambien el del teclado, y cierro el del mouse:
Wireless Mouse: menu cerrado
Mechanical Keyboard: menu ABIERTO
4) Desmonto la tarjeta del mouse (el usuario sale de esa vista):
Wireless Mouse: (desmontada, su menuOpen ya no existe)
Mechanical Keyboard: menu ABIERTO <- intacta
Lee la salida por pasos, porque cada uno demuestra una propiedad del estado local:
Paso 1 — dos celdas, dos valores iniciales. Las dos tarjetas arrancan con menu cerrado. Nada raro todavía, pero fíjate en que son dos celdas distintas, cada una con su propio false inicial. En React, cada useState(false) de cada ProductCard crea su propia celda; no comparten nada.
Paso 2 — el aislamiento, en acción. Abro el menú solo del mouse. El resultado: el mouse queda ABIERTO, y el teclado sigue cerrado. La tarjeta del teclado no se enteró —su menuOpen es una celda distinta, en otro cuarto—. Esta es la propiedad estrella del estado local: tocar la instancia A no toca la instancia B. Si menuOpen fuera un estado global compartido, abrir uno abriría todos, y tendrías un bug clásico ("abro el menú de un producto y se abren los de todos").
Paso 3 — cada uno con su propia vida. Abro el del teclado y cierro el del mouse: ahora el mouse está cerrado y el teclado ABIERTO. Las dos celdas evolucionan por separado, sin coordinarse. Cada tarjeta lleva su propia historia.
Paso 4 — la mortalidad. Desmonto la tarjeta del mouse (el usuario navegó fuera de esa vista). Su menuOpen deja de existir —"se fue con la instancia"—. La tarjeta del teclado sigue intacta, con su menú abierto. Esto es la limpieza automática: el estado local no se queda flotando cuando el componente se va. Si el usuario volviera a esa vista, se montaría una ProductCard nueva, con su menuOpen arrancando otra vez en false.
Ese es todo el modelo mental del estado local: una celda privada por instancia, aislada de las demás, que nace con el componente y muere con él. Simple, predecible, sin efectos a distancia. Por eso es la caja por defecto para todo lo que de verdad es privado de un componente.
Cómo reconocer que una pieza es local
La señal más confiable es una prueba mental de dos preguntas:
- ¿Alguien más, fuera de este componente, necesita leer o escribir este dato? Si la respuesta es "no —solo yo lo uso para mi propia UI—", es candidata a local. (Si "sí", probablemente sea global; lección 3.)
- ¿Tiene sentido que este dato desaparezca cuando el componente desaparece? Si "sí —cuando se cierra la vista, esta pregunta ya no importa—", confirma local. (Si el dato debería sobrevivir a un reload o a la navegación, probablemente sea url o server.)
Ejemplos que pasan las dos pruebas, y por lo tanto son local:
pieza por que es local
───────────────────────────── ────────────────────────────────────────────
menuOpen (dropdown de accion) solo la tarjeta lo usa; muere al cerrarse
input draft (texto sin enviar) solo el form lo usa mientras se escribe
isExpanded (acordeon) solo esa seccion lo usa; UI privada
isHovered (mouse encima) efimero, visual, de un solo elemento
activeTab (pestañas de un solo ese widget de tabs lo usa; si nadie
widget aislado) querria compartir la pestaña por link
Y una sutileza que la lección 7 medirá a fondo, pero que conviene sembrar ya: el borrador de un input —el texto que se está escribiendo, tecla por tecla, antes de confirmar— es local, aunque el valor confirmado termine en otra caja. En Mercado, mientras el usuario teclea en la SearchBar, ese texto a medias es local; pero en el momento en que aplica la búsqueda (presiona Enter), el query resultante pertenece a la URL (es compartible/recargable). La misma "barra de búsqueda" mezcla dos cajas según en qué momento de su ciclo esté el dato. Reconocerlo es parte de clasificar bien.
Errores comunes
Subir a global algo que era local. Qué pasa: se mete menuOpen, isHovered o activeTab en un Context o en un store global "para tenerlo todo junto". Por qué pasa: se confunde "muchos componentes del mismo tipo lo usan" con "es compartido". Cómo detectarlo: el store global tiene un menuOpen y de golpe todas las tarjetas abren su menú a la vez, o cambiar de pestaña en un widget afecta a otro. Cómo corregirlo: que muchas instancias de ProductCard tengan cada una un menuOpen no lo hace compartido —lo hace local repetido, una celda por instancia, que es exactamente lo que viste en el ejemplo—. Local no significa "único en la app"; significa "privado de cada instancia".
Levantar el estado más arriba de lo necesario. Qué pasa: se sube menuOpen al App (o a ProductList) para "controlarlo desde arriba", cuando solo la tarjeta lo usa. Por qué pasa: en react-fundamentals aprendiste a levantar el estado cuando dos componentes lo comparten, y se aplica por reflejo aunque aquí nadie lo comparta. Cómo detectarlo: el padre tiene estado que solo pasa a un hijo y que ningún otro hijo toca. Cómo corregirlo: el estado vive lo más abajo posible —en el componente que de verdad lo usa—. Levantar solo tiene sentido cuando dos o más componentes necesitan la misma pieza; si es uno solo, se queda local ahí. (Bajar el estado a donde se usa es tan importante como saber subirlo.)
Guardar en local lo que debía sobrevivir. Qué pasa: se guarda el filtro de búsqueda o la página en el useState de un componente, y al recargar o navegar se pierde. Por qué pasa: "es un valor que cambia, va en useState". Cómo detectarlo: el usuario recarga y su búsqueda desaparece; comparte el link y llega en blanco. Cómo corregirlo: aplica la prueba de la segunda pregunta —"¿tiene sentido que desaparezca cuando el componente desaparece?"—. Si la respuesta es "no, debería sobrevivir", no es local: es url (lección 5) o server (lección 4). Lo local es lo que sí puede morir sin que a nadie le duela.
Ejercicios
Ejercicio 1 — ¿Local o no? Para cada pieza, decide si es local y justifica con las dos preguntas ("¿alguien más lo necesita?" y "¿tiene sentido que muera con el componente?"): (a) si un tooltip de ayuda está visible al pasar el mouse; (b) qué producto está en el carrito; (c) el texto escrito en el campo "código de descuento" antes de aplicarlo; (d) si el acordeón de "detalles de envío" está expandido; (e) el término de búsqueda ya aplicado que filtra la lista de productos.
Ver solución
- (a) tooltip visible → local. Solo ese elemento lo usa, es efímero y visual, y muere sin problema cuando el mouse se va o el componente desaparece. Las dos preguntas dan "solo yo" y "sí, que muera".
- (b) producto en el carrito → NO local (es global). El carrito lo leen y escriben varios componentes lejanos (la tarjeta que agrega, el badge, el checkout), y no debería desaparecer porque un componente se desmonte. Primera pregunta: "sí, otros lo necesitan" → no es local. (Es global, lección 3.)
- (c) código de descuento sin aplicar → local. Igual que el borrador de la búsqueda: el texto a medio escribir es privado del form y puede morir con él. Cuando se aplica, el efecto (el descuento ya validado) puede pertenecer a otra caja, pero el borrador es local.
- (d) acordeón expandido → local. UI privada de esa sección; nadie más la necesita y muere sin dolor al cerrar la vista.
- (e) término de búsqueda aplicado → NO local (es url). Aquí está la trampa: mientras se escribe es local, pero el término ya aplicado que filtra la lista es algo que el usuario querría compartir/recargar → url (lección 5). La segunda pregunta lo delata: "no, no debería morir con el componente; debería sobrevivir a un reload".
Ejercicio 2 — El aislamiento, explicado. Con el ejemplo ejecutado, explica por qué abrir el menú de la tarjeta del mouse no abrió el del teclado. ¿Qué pasaría si menuOpen fuera una única variable global compartida por todas las tarjetas? Describe el bug concreto que verías en pantalla.
Ver solución
En el ejemplo, cada makeProductCard crea su propia celda menuOpen en un closure; la del mouse y la del teclado son dos celdas distintas. Por eso mouseCard.toggleMenu() solo tocó la celda del mouse, y el teclado —que lee otra celda— siguió cerrado. Es el aislamiento por instancia: en React, cada useState(false) de cada ProductCard es una celda separada.
Si menuOpen fuera una sola variable global compartida por todas las tarjetas, tocar el botón "···" de cualquier producto pondría ese único menuOpen en true, y todas las tarjetas —que leerían la misma variable— mostrarían su menú a la vez. El bug en pantalla: abres el menú de acciones de un producto y se despliegan los menús de los veinte productos de la lista al mismo tiempo. Es el clásico "todos los dropdowns se abren juntos", y su causa es exactamente esa: un estado que debía ser local por instancia se puso global compartido.
Ejercicio 3 — Baja el estado a su lugar. Un compañero puso el menuOpen de las tarjetas en el App (la raíz), pasándolo por props hasta cada ProductCard. Explica por qué eso está mal clasificado, qué problemas trae, y cómo lo corregirías. ¿Cambia tu respuesta si el requisito fuera "solo un menú puede estar abierto a la vez en toda la lista"?
Ver solución
Poner un único menuOpen en el App está mal por dos razones. Primera, clasificación: menuOpen es local de cada tarjeta (nadie fuera de la tarjeta lo necesita), así que su lugar es el useState de la propia ProductCard, lo más abajo posible. Segunda, comportamiento: un solo menuOpen en la raíz haría que todas las tarjetas compartan el mismo valor → todos los menús se abren juntos (el bug del ejercicio 2). La corrección: bajar el estado a la ProductCard (const [menuOpen, setMenuOpen] = useState(false) dentro de cada tarjeta), para que cada instancia tenga el suyo.
Si el requisito fuera "solo uno abierto a la vez", la respuesta cambia: ahora sí hay coordinación entre tarjetas —cuál está abierta es una pregunta sobre la lista, no sobre una tarjeta suelta—. Ese estado (openCardId) se levanta al padre común (ProductList), que sabe cuál es la abierta y se lo pasa a cada tarjeta. No se vuelve global de app; se levanta solo hasta el ancestro común que necesita coordinar (justo lo que aprendiste en react-fundamentals: levantar al punto exacto donde se comparte, ni más arriba ni más abajo). La lección: la caja de una pieza depende de quién la necesita, y ese "quién" puede ser un componente (local), un subárbol (levantar al padre) o toda la app (global).
Resumen y siguiente paso
En esta lección abriste la primera gaveta: el estado local, la UI privada de un componente. Vimos sus dos propiedades definitorias, ejecutadas: el aislamiento por instancia —dos ProductCard con dos menuOpen que no se enteran uno del otro— y la mortalidad —el estado local nace con el componente y muere cuando se desmonta—. Aprendiste a reconocerlo con dos preguntas ("¿alguien más lo necesita?", "¿tiene sentido que muera con el componente?") y viste la sutileza del borrador de un input, que es local aunque su valor confirmado pertenezca a otra caja. La herramienta —useState, de react-fundamentals— la diste por sabida; lo nuevo fue clasificar con seguridad qué pertenece aquí.
Antes de avanzar deberías poder: definir el estado local y sus dos propiedades; distinguir "local repetido por instancia" de "compartido"; aplicar las dos preguntas de reconocimiento; y explicar por qué levantar o globalizar algo que era local trae bugs (todos los menús abriéndose juntos, estado que ensucia la app).
La lección 3 abre la segunda gaveta: el estado global de cliente, lo que varios componentes lejanos comparten. Ahí verás, ejecutado, el caso que el estado local no puede resolver: el cart que la ProductCard escribe y el badge del header lee, dos componentes que no son padre-hijo y que necesitan la misma verdad. Es el momento en que el aislamiento —virtud del estado local— se vuelve un estorbo, y aparece la necesidad de una sola verdad compartida.
Recursos
- React, "State: A Component's Memory" — react.dev/learn/state-a-components-memory. La página oficial de
useState: qué es el estado de un componente y cómo cada instancia tiene el suyo. El repaso de la herramienta que esta caja usa. En inglés. - React, "Choosing the State Structure" — react.dev/learn/choosing-the-state-structure. Principios para decidir dónde y cómo guardar estado; en particular, mantenerlo lo más local posible y no duplicarlo. En inglés.
- React, "Sharing State Between Components" — react.dev/learn/sharing-state-between-components. El contraste con esta lección: cuándo el estado deja de ser local y hay que levantarlo. El puente hacia la lección 3. En inglés.
- React, "Preserving and Resetting State" — react.dev/learn/preserving-and-resetting-state. Qué le pasa al estado local cuando un componente se monta, desmonta o cambia de posición: la "mortalidad" que medimos en el ejemplo. En inglés.