Estado
En resumen: El estado es un valor que un componente recuerda entre renders; en React se guarda con useState, y cada cambio vuelve a renderizar el componente.
Problema
El botón no cuenta
La tarjeta del producto tiene un botón «Añadir» y un contador «En el carrito». Pulsa «Añadir» tres veces.
Consola
La consola dice count = 1, 2, 3. La tarjeta dice 0. No hay ningún error. La variable count cambia, pero la pantalla no la sigue. ¿Dónde está la diferencia?
La idea
Una variable normal no basta
La tarjeta no la dibuja tu código, sino la biblioteca. Solo vuelve a dibujar cuando sabe que algo cambió. count++ cambia la variable, pero no avisa a nadie.
ProductCard es una función. React la llama cuando dibuja la tarjeta, y ella devuelve lo que hay que mostrar. let count = 0 es una variable local de esa llamada. El clic la cambia, pero nadie vuelve a llamar a la función, así que la tarjeta se queda en 0.
Hay un segundo problema. Si React vuelve a llamar a la función por otro motivo, la primera línea es otra vez let count = 0. Todo lo que se había acumulado en la variable se pierde.
Necesitas datos que el componente recuerde entre dos renders y cuyo cambio lo vuelva a dibujar. Esto se llama estado.
La idea
useState y ref
const [count, setCount] = useState(0);
function add() { setCount(count + 1);}useState(0)devuelve dos cosas: el valor y la función que lo cambia. El cero es solo el valor inicial.countes el valor de este render.setCount(1)recuerda el valor nuevo y le dice a React que vuelva a dibujar la tarjeta. React llama otra vez aProductCard, y esta vezuseStatedevuelve 1.
useState es un hook: una función de React que se llama al principio del componente, nunca dentro de un if ni de un bucle. React distingue los estados por el orden en que se llaman.
El manejador del clic se da como función: onClick={add}. Si escribes onClick={add()}, add se llama mientras se dibuja la tarjeta. Cambia el estado, que dibuja otra vez, y React se detiene con el error «Too many re-renders».
Consola
Míralo
Mira qué pasa con un clic
function ProductCard({ product }) { const [count, setCount] = useState(0);
function addThree() { setCount(count + 1); setCount(count + 1); setCount(count + 1); }
return ( <article> <button onClick={addThree}>+3</button> <p>En el carrito: {count}</p> </article> );}Paso a paso, en palabras
- La tarjeta guarda count en el estado de React: useState(0) da el valor y la función que lo cambia.
- React llama a la función ProductCard. No toca la página, sino que devuelve una descripción: un botón y un texto con count = 0.
- La primera vez, la descripción se convierte en elementos reales en el DOM. La página ya muestra «En el carrito: 0».
- Un clic en +3: el navegador llama a addThree.
- Tres veces setCount(count + 1). En esta llamada count es una instantánea: 0. Cada vez va a la cola «pasa a 1». La pantalla todavía no cambia.
- El manejador terminó. React aplica la cola: 1, 1, 1 da 1. Luego vuelve a llamar a ProductCard, y ahora count es 1.
- React compara la descripción nueva con la vieja. El botón es el mismo, solo cambió el texto. En el DOM cambia solo el texto.
- Por eso el contador muestra 1, no 3. Cuando el valor nuevo depende del viejo, pasa una función: setCount((c) => c + 1).
La tarjeta no se construye de nuevo. La biblioteca compara la descripción nueva con la vieja y cambia en el DOM solo la diferencia: el texto. El botón es el mismo elemento y el foco se queda en él. Esa es la diferencia con render() del módulo del DOM, que lo reemplazaba todo.
Pruébalo
Arregla el contador
Esta es la tarjeta del principio. Haz que count sea estado, para que después de cada clic en «Añadir» la tarjeta muestre el número nuevo.
Consola
Solución
ProductCard.jsximport { useState } from 'react';
const money = (x) => x.toFixed(2).replace('.', ',') + ' €';
export default function ProductCard({ product }) {
const [count, setCount] = useState(0);
function add() {
setCount(count + 1);
}
return (
<article className="card">
<h2>{product.name}</h2>
<p>{money(product.price)}</p>
<button type="button" onClick={add} aria-label={`Añadir ${product.name}`}>
Añadir
</button>
<p>En el carrito: {count}</p>
</article>
);
}
App.jsximport ProductCard from './ProductCard';
const product = { id: 1, name: 'Pan blanco', price: 1.2 };
export default function App() {
return (
<main>
<h1>La tienda</h1>
<ProductCard product={product} />
</main>
);
}
style.cssbody { font-family: system-ui, sans-serif; margin: 0; padding: 8px 16px; }
h1 { font-size: 22px; }
.card { border: 1px solid #ddd5c4; border-radius: 8px; padding: 4px 16px 12px; margin-bottom: 12px; max-width: 340px; }
.card h2 { font-size: 18px; margin: 8px 0 4px; }
.card p { margin: 6px 0; }
button { font: inherit; padding: 4px 12px; margin-right: 6px; }
let count = 0 pasa a ser const [count, setCount] = useState(0), y count++ pasa a ser setCount(count + 1). Hay que importar useState: import { useState } from 'react'. Si count++ se queda, el valor cambia, pero React no vuelve a dibujar.La idea
El valor es una instantánea
El agua mineral también se vende en un pack de 6 botellas. Sería cómodo que el botón del pack llamara seis veces a add(). Aquí las dos bibliotecas se comportan de forma distinta.
count es una constante de un render. setCount no cambia count en el manejador que se está ejecutando ahora. Pone una petición en una cola, y React la aplica después del manejador:
function addBox() { for (let i = 0; i < 6; i++) { setCount(count + 1); // 0 + 1, seis veces: «pasa a 1» }}Las seis peticiones son «pasa a 1» y el contador pasa a 1, no a 6. Di en su lugar «suma 1 al último valor»: pasa una función en vez de un valor.
setCount((c) => c + 1);React llama a cada una de esas funciones con el resultado de la anterior: 0 → 1 → 2 … → 6. La regla: cuando el valor nuevo depende del viejo, pasa una función.
Pruébalo
Encuentra el error
«Añadir un pack» debería añadir 6 botellas. ¿Qué línea tiene la culpa?
Elige la línea con el error.
Línea 5: count es 0 en cada una de las seis llamadas, así que las peticiones son «pasa a 1». Debería ser setCount((c) => c + 1) o directamente setCount((c) => c + 6).
Pruébalo
Un pack de agua
Escribe addBox() para que «Añadir un pack (6 uds.)» sume 6 botellas a lo que ya hay en el carrito. La comprobación pulsa «Añadir» una vez, luego el pack dos veces, y espera 1, 7 y 13.
Consola
Solución
ProductCard.jsximport { useState } from 'react';
const money = (x) => x.toFixed(2).replace('.', ',') + ' €';
export default function ProductCard({ product }) {
const [count, setCount] = useState(0);
function add() {
setCount((c) => c + 1);
}
function addBox() {
setCount((c) => c + 6);
}
return (
<article className="card">
<h2>{product.name}</h2>
<p>{money(product.price)}</p>
<button type="button" onClick={add} aria-label={`Añadir ${product.name}`}>
Añadir
</button>
<button type="button" onClick={addBox}>Añadir un pack (6 uds.)</button>
<p>En el carrito: {count}</p>
</article>
);
}
App.jsximport ProductCard from './ProductCard';
const product = { id: 15, name: 'Agua mineral', price: 0.9 };
export default function App() {
return (
<main>
<h1>La tienda</h1>
<ProductCard product={product} />
</main>
);
}
style.cssbody { font-family: system-ui, sans-serif; margin: 0; padding: 8px 16px; }
h1 { font-size: 22px; }
.card { border: 1px solid #ddd5c4; border-radius: 8px; padding: 4px 16px 12px; margin-bottom: 12px; max-width: 340px; }
.card h2 { font-size: 18px; margin: 8px 0 4px; }
.card p { margin: 6px 0; }
button { font: inherit; padding: 4px 12px; margin-right: 6px; }
setCount((c) => c + 6) suma 6 al último valor. Seis veces setCount(count + 1) da 2: count es 1 en cada una. setCount(6) no suma, pone 6, y después de «Añadir» el contador es 6, no 7. En la solución add también usa una función: así ninguno de los dos botones depende de la instantánea.La idea
Cada tarjeta recuerda lo suyo
El estado pertenece a la instancia, no al código. El catálogo tiene dos tarjetas del mismo ProductCard, y cada una tiene su propio count:
Consola
Pulsa «Añadir» dos veces en el pan y una en la leche: 2 y 1. La biblioteca guarda el estado de cada tarjeta según su lugar en el árbol de componentes. Si quitas la tarjeta de la página, su estado se va con ella. Cuando dos tarjetas tienen que mostrar el mismo número, este tiene que vivir en su padre común. De eso trata la lección sobre estado compartido de este módulo.
Más precisamente, para curiosos
Por qué una instantánea. React no cambia count en el sitio, porque la función se llamó con ese valor, y todo lo que hay en ella, también los manejadores, ve ese valor. Así un render es coherente: todo lo que se calcula a partir de count se calcula a partir de un único valor.
Cuándo React no dibuja. Si setCount recibe el mismo valor (comparado con Object.is), React se salta el render. Esto importa en la lección siguiente, con los arrays.
Las actualizaciones se agrupan. Todas las llamadas a setCount en un manejador dan un solo render, y desde React 18 también dentro de setTimeout y después de await.
StrictMode. En desarrollo, <StrictMode> llama a cada componente dos veces, para detectar un componente que cambia algo fuera mientras se dibuja. El sandbox no lo usa, y un proyecto de Vite lo tiene en main.jsx.
Componentes de clase. El código antiguo de React escribe el componente como una clase con this.state y this.setState. El código nuevo usa funciones y hooks.
Ejercicio
Elige la cantidad
El catálogo tiene tres tarjetas. En cada una añade una cantidad que empieza en 1, un botón «−» (nombre para el lector de pantalla: «Menos Pan blanco»), un botón «+» («Más Pan blanco») y una línea «2 × 1,20 € = 2,40 €» con la cantidad, el precio y el total. «−» está desactivado cuando la cantidad es 1. Cada tarjeta cuenta por su cuenta.
Consola
Solución
ProductCard.jsximport { useState } from 'react';
const money = (x) => x.toFixed(2).replace('.', ',') + ' €';
export default function ProductCard({ product }) {
const [count, setCount] = useState(1);
function less() {
setCount(count - 1);
}
function more() {
setCount(count + 1);
}
return (
<article className="card">
<h2>{product.name}</h2>
<p>{money(product.price)}</p>
<p>
<button type="button" onClick={less} disabled={count === 1} aria-label={`Menos ${product.name}`}>−</button>
<button type="button" onClick={more} aria-label={`Más ${product.name}`}>+</button>
</p>
<p>{count} × {money(product.price)} = {money(count * product.price)}</p>
</article>
);
}
App.jsximport ProductCard from './ProductCard';
const products = [
{ id: 1, name: 'Pan blanco', price: 1.2 },
{ id: 5, name: 'Leche fresca', price: 1.6 },
{ id: 7, name: 'Queso blanco', price: 7.9 },
];
export default function App() {
return (
<main>
<h1>La tienda</h1>
{products.map((p) => (
<ProductCard key={p.id} product={p} />
))}
</main>
);
}
style.cssbody { font-family: system-ui, sans-serif; margin: 0; padding: 8px 16px; }
h1 { font-size: 22px; }
.card { border: 1px solid #ddd5c4; border-radius: 8px; padding: 4px 16px 12px; margin-bottom: 12px; max-width: 340px; }
.card h2 { font-size: 18px; margin: 8px 0 4px; }
.card p { margin: 6px 0; }
button { font: inherit; padding: 4px 12px; margin-right: 6px; }
ProductCard: const [count, setCount] = useState(1), less llama a setCount(count - 1), y more llama a setCount(count + 1). «−» es disabled={count === 1}: así la cantidad no puede bajar de 1. El total es money(count * product.price) directamente en el JSX: se deduce de count y no es un estado aparte. De eso trata la lección sobre estado derivado.Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Eventos · El primer componente · Props: datos desde arriba · Listas y claves
- Continúa en: Actualizar objetos y arrays · Estado derivado · Estado compartido: hacia arriba · Efectos
- Ejercicios sobre el tema: Estado