Módulo 3: State With Usestate
Estado vs props: prestado vs propio
Descripción
Ahora que sabes que un componente puede tener estado —memoria propia que persiste y, al cambiar, repinta—, aparece de inmediato la pregunta que hay que resolver antes que ninguna otra: cuando tienes un dato, ¿va en props o en estado? Elegir mal es el error más frecuente del principiante, y produce bugs escurridizos: pantallas que no se actualizan cuando deberían, datos que se desincronizan, componentes imposibles de reutilizar. Esta lección te da la brújula para no equivocarte.
La distinción, en una frase: las props son datos prestados; el estado son datos propios. Desmenucémosla, porque cada mitad tiene consecuencias.
Las props vienen del padre. Bajan por el árbol (lo viste en el módulo 1: los datos fluyen hacia abajo), y desde el punto de vista del hijo son de solo lectura: el hijo las usa, pero nunca las cambia. Si ProductCard recibe name por props, muestra ese nombre, pero no le pertenece —es del catálogo que posee App—. Las props son como un dato que te confían para que lo uses en tu tarea; lo lees, no lo tachas.
El estado es propio del componente. No viene de afuera: lo declara el componente con useState, vive en su libreta, persiste entre renders, y —esta es la diferencia operativa clave— el componente lo cambia él mismo, con su setter. Si la SearchBar tiene un estado query, ese texto es suyo: nadie se lo pasa, ella lo guarda y lo actualiza cuando el usuario escribe.
Conexión con el módulo. Esta lección es la brújula del módulo entero. El estado (todo lo que viene: snapshot, re-render, updater, inmutabilidad) solo tiene sentido una vez que sabes qué debe ser estado. Y se apoya directo en lo que aprendiste antes: las props y su flujo unidireccional son del módulo 1; aquí las contrastamos con el estado para que veas que no compiten, se complementan. Un componente típico recibe algunas props (lo que le dan) y tiene algún estado (lo que es suyo y cambia), y produce su UI a partir de los dos. Saber cuál es cuál es lo que te deja construir sin enredarte.
Una analogía: el formulario impreso y tus anotaciones
Imagina que llegas a una oficina a hacer un trámite y te entregan un formulario impreso. Arriba, ya escrito con tinta, viene tu nombre, tu número de expediente, la fecha de la cita: datos que la oficina puso ahí. Tú no los escribiste ni los puedes cambiar —son parte del formulario tal como te lo dieron—. Si tu nombre está mal impreso, no lo tachas y reescribes por tu cuenta; le avisas a la oficina, que es la dueña de ese dato. Esos datos impresos son las props: te llegan de arriba (la oficina, el padre), los usas, pero no son tuyos para cambiar.
Ahora, ese mismo formulario tiene campos en blanco que tú llenas: tu firma, la opción que eliges, una nota que agregas. Eso lo escribes tú, es tuyo, y lo puedes cambiar mientras rellenas —tachar tu propia nota y poner otra—. Esos campos que tú llenas y controlas son el estado: datos propios, que tú cambias, que forman parte de tu interacción con el formulario.
El trámite completo —lo que la oficina finalmente procesa— es la combinación de las dos cosas: lo que venía impreso (props) más lo que tú llenaste (estado). Así es un componente: produce su UI combinando lo que le dieron (props) con lo que es suyo y cambia (estado). Y la regla de oro de la analogía es la que no debes olvidar: no reescribas la parte impresa (no mutes las props), y lo que llenas es solo tuyo (tu estado no lo ve ni lo cambia otro; es privado de tu formulario). Guarda la imagen: props = lo impreso por otro; estado = lo que tú llenas.
Ejemplo trabajado: la SearchBar con una prop fija y un estado que cambia
Vamos a ver la distinción ejecutada. Construiremos una SearchBar que recibe una prop (placeholder, el texto gris de "Search products...") y tiene un estado (query, lo que el usuario escribe). Correremos varios renders y observaremos la asimetría central: la prop no cambia entre renders (viene del padre, que pasa siempre la misma), mientras el estado sí cambia (es propio, y lo actualizamos con su setter). Y al final comprobaremos, con un error real, que la prop es de solo lectura.
Así se escribe en React de verdad. Fíjate en las dos fuentes de datos: props.placeholder (prestado) y query (propio, del useState):
import { useState } from 'react';
function SearchBar(props) {
const [query, setQuery] = useState(''); // ESTADO: propio, arranca vacio
return (
<input
className="search-bar"
placeholder={props.placeholder} // PROP: viene del padre
value={query} // ESTADO: propio del componente
/>
);
}
function App() {
return <SearchBar placeholder="Search products..." />; // el padre pasa la prop
}
Lee el reparto de papeles: App (el padre) le pasa a SearchBar la prop placeholder. Dentro de SearchBar, props.placeholder es de solo lectura —el texto gris que el padre decidió—, mientras query es estado propio que la barra guarda y cambia. (El onChange que dispararía setQuery cuando el usuario teclea es el módulo 4; aquí llamamos al setter "a mano" para ver la mecánica.)
Ahora la versión ejecutable en Node. Modelamos el estado con una celda que persiste (como en la lección 1) y hacemos que el padre pase una prop congelada, para que se vea que el hijo no la puede cambiar:
'use strict';
// ── El estado vive en una celda que PERSISTE entre renders ──
// (En React, esta celda la maneja useState; aqui la modelamos a mano
// para ver la diferencia con las props.)
let queryCell = ''; // estado inicial de SearchBar: cadena vacia
function setQuery(next) {
// el SETTER: la unica via legitima de cambiar el estado propio
queryCell = next;
}
// El componente recibe PROPS (de afuera, de solo lectura) y lee su ESTADO (propio).
function SearchBar(props) {
const query = queryCell; // LEE el estado desde su celda
return {
placeholder: props.placeholder, // PROP: viene del padre
value: query, // ESTADO: propio del componente
};
}
// El padre pasa las props. Van congeladas para hacer visible que el hijo
// no las puede cambiar.
const propsFromParent = Object.freeze({ placeholder: 'Search products...' });
console.log('=== las PROPS no cambian; el ESTADO si, entre renders ===');
console.log('render 1:', JSON.stringify(SearchBar(propsFromParent)));
// El usuario "escribe": el componente cambia SU estado con el setter.
setQuery('mo');
console.log('render 2:', JSON.stringify(SearchBar(propsFromParent)));
setQuery('mouse');
console.log('render 3:', JSON.stringify(SearchBar(propsFromParent)));
// Un hijo NO puede cambiar sus props: pertenecen al padre.
console.log('\n=== un hijo intenta cambiar una PROP ===');
try {
propsFromParent.placeholder = 'otra cosa'; // mutar prop congelada
} catch (e) {
console.log(` X ${e.constructor.name}: ${e.message}`);
}
console.log('la prop sigue igual:', JSON.stringify(propsFromParent.placeholder));
Qué esperar. Al correr el archivo, la salida es exactamente esta:
=== las PROPS no cambian; el ESTADO si, entre renders ===
render 1: {"placeholder":"Search products...","value":""}
render 2: {"placeholder":"Search products...","value":"mo"}
render 3: {"placeholder":"Search products...","value":"mouse"}
=== un hijo intenta cambiar una PROP ===
X TypeError: Cannot assign to read only property 'placeholder' of object '#<Object>'
la prop sigue igual: "Search products..."
Esta salida es la distinción entera, medida. Léela en las dos columnas de cada render.
Mira la columna del placeholder a lo largo de los tres renders: "Search products...", "Search products...", "Search products...". No cambia nunca. Y no cambia por una razón concreta: es una prop, y el padre le pasa siempre la misma. Para que cambiara, tendría que cambiarla el padre (llamando a SearchBar con otro placeholder), no la barra. Desde dentro de SearchBar, placeholder es un dato fijo, prestado, que solo se lee.
Ahora mira la columna del value (el estado query): "", "mo", "mouse". Cambia en cada render, y cambia porque la barra misma lo cambió con setQuery. Es su dato propio, guardado en la celda que persiste, actualizado por su setter. Nadie de afuera lo tocó; la barra lo gestiona sola. Esa es la asimetría de fondo: la prop es constante porque no le pertenece al componente; el estado varía porque sí.
Y la última parte clava por qué las props no se cambian desde el hijo. SearchBar (o cualquier hijo) que intentara props.placeholder = 'otra cosa' choca con un TypeError: Cannot assign to read only property 'placeholder', porque la prop está congelada (Object.freeze) —tal como, en React, las props son de solo lectura por disciplina—. La prop sigue valiendo "Search products..." después del intento: no se pudo cambiar. Esto es la mitad "las props son prestadas" hecha error visible: el dato es del padre, y el hijo no lo reescribe. (Lo viste ya en el módulo 1; aquí lo recuperamos para contrastarlo con el estado, que sí se cambia —pero con su setter, no con una asignación—.)
La tabla que resuelve casi todas las dudas
Cuando no sepas si un dato va en props o en estado, esta comparación resuelve la mayoría de los casos:
PROPS ESTADO
───────────────── ─────────────────────────── ───────────────────────────
De donde viene del padre (de afuera) del propio componente
Quien lo cambia el padre (nunca el hijo) el componente, con su setter
Desde el hijo es de solo lectura propio, editable (via setter)
Persiste? mientras el padre lo pase si, entre renders (en la libreta)
Para que sirve configurar/alimentar al hijo recordar lo que cambia con el tiempo
Ejemplo (Mercado) product.name, placeholder query, expanded, cartCount
Fíjate en el par de filas centrales: quién lo cambia y desde el hijo es. Ahí está el corazón de la distinción. Si el dato lo cambia el componente mismo en respuesta a algo (el usuario escribe, da clic), es estado. Si lo cambia (o lo fija) quien está más arriba y el componente solo lo recibe, es props.
No compiten: un mismo componente tiene de los dos
Un error de encuadre es pensar que un componente es "de props" o "de estado". Casi todos tienen de los dos. La SearchBar del ejemplo recibe placeholder por props (configuración que le da el padre) y tiene query en estado (lo que el usuario escribe, que es suyo). Un ProductCard recibe el producto por props (name, priceCents, del catálogo de App) y puede tener expanded en estado (si el usuario expandió esa tarjeta). La UI se describe combinando ambos:
flowchart TD
Parent["App (el padre)"] -->|"props: placeholder<br/>(prestado, de solo lectura)"| SB["SearchBar"]
State["useState('') -> query<br/>(propio, cambia con setQuery)"] --> SB
SB --> UI["UI = f(props, estado)<br/><input placeholder=... value=query>"]
Las props entran por arriba (del padre); el estado nace dentro; y la UI es función de los dos. No hay competencia: hay reparto de papeles.
Errores comunes
Copiar una prop en estado "para poder cambiarla". Qué pasa: un componente recibe un dato por props y, como quiere modificarlo, lo copia a estado en el arranque: const [name, setName] = useState(props.name). Por qué pasa: uno choca con que las props son de solo lectura y "resuelve" duplicándolas en algo editable. Cómo detectarlo: tienes un estado inicializado con una prop, y esperas que se actualice cuando la prop cambie —pero no lo hace, porque el valor inicial de useState solo se usa la primera vez (lección 3)—. El resultado es un estado que se desincroniza de la prop: el padre cambia el dato, pero tu copia en estado sigue con el valor viejo. Cómo corregirlo: pregúntate por qué quieres "cambiar" la prop. Si es para mostrar una versión transformada, calcula un valor derivado sin estado (const displayName = props.name.toUpperCase()). Si de verdad es un dato que el usuario controla y que difiere de la prop, entonces ese dato es estado legítimo desde el principio —pero no lo llames "copia de la prop"; es otra cosa—. La regla: no dupliques en estado algo que ya es una prop.
Poner en estado algo que se puede derivar. Qué pasa: se guarda en estado un dato que en realidad se calcula a partir de otro estado o de props —el precio formateado, el total del carrito, la lista filtrada—. Por qué pasa: parece práctico "tenerlo guardado". Cómo detectarlo: tienes dos piezas de estado y una es siempre función de la otra; para mantenerlas coherentes tienes que acordarte de actualizar las dos juntas, y tarde o temprano se te olvida y se desincronizan. Cómo corregirlo: guarda en estado solo la fuente que cambia por sí misma, y calcula el resto en el render (const total = items.reduce(...)). Una pieza de estado por cosa que cambia; lo demás se deriva. La regla la formalizamos en la lección 7 y el módulo 5 la desarrolla; por ahora, sospecha de cualquier estado que sea "el resultado de" otro dato.
Tratar como fija una prop que en realidad es estado (o al revés). Qué pasa: alguien pone en props algo que el propio componente debe cambiar (y entonces no puede cambiarlo), o pone en estado algo que en realidad viene del padre (y entonces se desincroniza). Por qué pasa: no se hizo la pregunta de "¿quién cambia este dato?". Cómo detectarlo: intentas actualizar una prop desde el hijo (imposible, es de solo lectura), o tienes un estado que "debería" seguir a un dato del padre pero no lo hace. Cómo corregirlo: para cada dato, responde ¿quién lo cambia? Si lo cambia el componente en respuesta a una interacción, es estado. Si lo fija o lo cambia quien está más arriba, es props. Y si un dato es propio de un componente pero otro también lo necesita, no lo dupliques: levanta el estado al ancestro común (módulo 7) y bájalo por props.
Ejercicios
Ejercicio 1 — Clasifica cada dato. Para un ProductCard de Mercado que muestra un producto y permite expandirlo para ver detalles, clasifica cada dato como props o estado y justifica: (a) name; (b) priceCents; (c) expanded (si la tarjeta muestra los detalles largos); (d) onAddToCart (una función que el padre le pasa para avisar cuando se agrega al carrito).
Ver solución
- (a)
name: props. Viene del catálogo, que poseeApp; elProductCardsolo lo muestra, no lo cambia. Dato prestado. - (b)
priceCents: props. Igual que el nombre: parte del producto, prestado desde arriba, de solo lectura. - (c)
expanded: estado. Es una cosa que cambia con el tiempo por acción del usuario (da clic para expandir/colapsar), es propia de esa tarjeta, y hay que recordarla. Vive comoconst [expanded, setExpanded] = useState(false). - (d)
onAddToCart: props. Aunque es una función y no un dato "de mostrar", igual viene del padre (App, que posee el carrito). ElProductCardla recibe y la llama, pero no la define ni la cambia. Las funciones que baja el padre son props como cualquier otra (las estudiamos como callbacks en el módulo 4).
El patrón: lo que el ProductCard recibe de arriba (datos del producto, funciones para avisar) es props; lo que es suyo y cambia por su cuenta (si está expandido) es estado.
Ejercicio 2 — El bug de la copia. Este componente intenta mostrar el nombre del producto en mayúsculas copiando la prop a estado. Explica por qué se va a desincronizar y reescríbelo correctamente:
function ProductCard(props) {
const [name, setName] = useState(props.name.toUpperCase());
return <h3>{name}</h3>;
}
Ver solución
Por qué se desincroniza: el valor inicial de useState (aquí props.name.toUpperCase()) solo se usa en el primer render; en los renders siguientes, useState ignora ese argumento y devuelve lo que ya hay en la libreta (esto es la lección 3). Así que si el padre cambia props.name —porque el catálogo se actualizó, o porque esta misma tarjeta se reutiliza para otro producto—, el estado name sigue con el valor viejo en mayúsculas. La tarjeta muestra un nombre que ya no corresponde al producto. El estado quedó "atascado" en la foto del primer render.
Por qué está mal de raíz: el nombre en mayúsculas no es estado. No es un dato que el usuario cambie ni que el componente deba recordar; es simplemente una transformación de una prop, que se puede recalcular en cada render.
Reescrito, como valor derivado sin estado:
function ProductCard(props) {
const displayName = props.name.toUpperCase(); // se recalcula cada render
return <h3>{displayName}</h3>;
}
Ahora displayName se computa a partir de props.name en cada render. Si el padre cambia el nombre, la tarjeta lo recalcula y siempre está sincronizada. No hay estado que mantener ni que se pueda quedar atrás. La regla: no copies una prop en estado; si necesitas una versión transformada, derívala en el render.
Ejercicio 3 — ¿Por qué no compiten? Un compañero pregunta: "si el estado es 'mejor' porque el componente lo controla, ¿por qué no hago todo con estado y me olvido de las props?". Explícale por qué props y estado cumplen papeles distintos y por qué una app necesita los dos, con un ejemplo del storefront.
Ver solución
Porque resuelven problemas distintos, y confundirlos rompe cosas. El estado sirve para lo que un componente posee y cambia por su cuenta; las props sirven para pasar datos de un componente a otro, de arriba hacia abajo. Sin props, los componentes no podrían comunicarse: cada uno sería una isla incapaz de recibir información de su padre.
Ejemplo del storefront: el catálogo de productos lo posee App (es su estado, o le llega de un fetch). ¿Cómo llega cada producto a su ProductCard? Por props: App baja la lista a ProductList, que baja cada producto a un ProductCard. Si intentaras que cada ProductCard tuviera "su propio estado" con el producto, ¿de dónde lo sacaría? El dato vive arriba; tiene que bajar por props. Las props son el mecanismo de reparto.
Y al revés: el texto que el usuario escribe en la SearchBar es de la barra; no tiene sentido que se lo pase el padre, porque nace de la interacción dentro de la barra. Eso es estado.
En una app real, el dato suele nacer como estado en algún componente (quien lo posee) y viajar como props hacia los que lo necesitan más abajo. Estado para poseer y cambiar; props para repartir hacia abajo. No compiten: son las dos mitades de cómo fluyen los datos en React. (Cuando un dato es estado de un componente pero varios lo necesitan, se levanta al ancestro común y baja por props —módulo 7—; ahí verás las dos mitades trabajando juntas.)
Resumen y siguiente paso
En esta lección afilaste la brújula del módulo: estado vs props. Las props son datos prestados —vienen del padre, bajan por el árbol, y desde el hijo son de solo lectura—; el estado es dato propio —vive en el componente, persiste entre renders, y lo cambia el componente mismo con su setter—. Lo mediste ejecutando una SearchBar: su prop placeholder fue constante en los tres renders (la fija el padre), mientras su estado query cambió de "" a "mo" a "mouse" (lo cambia ella con setQuery); y viste el TypeError al intentar reescribir la prop congelada. Te llevas la pregunta que resuelve casi todo: ¿quién cambia este dato? Si el componente, es estado; si quien está arriba, es props. Y la advertencia de no duplicar en estado lo que es prop (se desincroniza) ni guardar lo que se puede derivar.
Antes de avanzar deberías poder: decir de dónde viene cada uno (props del padre, estado del propio componente) y quién lo cambia; clasificar un dato del storefront como props o estado justificándolo; detectar el bug de copiar una prop en estado; y entender que un componente típico tiene de los dos y no compiten.
La lección 3 baja al detalle de la herramienta con la que declaras estado: el hook useState. Vas a ver, ejecutada, la API real —const [query, setQuery] = useState('')—: qué devuelve exactamente (un par: el valor y el setter), por qué se desestructura con corchetes, qué es el valor inicial y por qué solo se usa en el primer render, y las reglas que hay que respetar para que los hooks funcionen. Pasarás de "sé qué es el estado" a "sé escribirlo".
Recursos
- React, "Passing Props to a Component" — react.dev/learn/passing-props-to-a-component. La página oficial sobre las props: cómo se pasan de padre a hijo y por qué son de solo lectura. La mitad "props" de esta lección. En inglés.
- React, "State: A Component's Memory" — react.dev/learn/state-a-components-memory. La página oficial sobre el estado como memoria propia del componente. La mitad "estado" de esta lección. En inglés.
- React, "Choosing the State Structure" — react.dev/learn/choosing-the-state-structure. Guía oficial sobre qué debe ser estado y qué no (no dupliques, no guardes lo derivado); profundiza la brújula de esta lección. En inglés.
- MDN, "Object.freeze()" — developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze. Cómo funciona el congelado que usamos para hacer visible que las props son de solo lectura. En inglés.