Estado con useState
Las props te permiten pasar datos de un padre a un hijo, pero son de solo lectura. ¿Y si un componente necesita datos que cambian? Un contador que incrementa, un campo de texto que el usuario rellena, un menú que se abre y se cierra. Para eso necesitas estado.
Si las props son los parámetros que recibe una función (como aprendiste en Lógica de Programación), el estado es la memoria interna del componente — datos que el componente controla y puede modificar.
El hook useState
useState es el hook más básico de React. Un hook es una función especial que empieza con use y te permite "engancharte" a funcionalidades de React. useState te permite añadir estado a un componente.
import { useState } from 'react'
function Contador() {
// useState devuelve un array: [valor, funcionParaActualizar]
const [cuenta, setCuenta] = useState(0)
return (
<div>
<p>Has pulsado {cuenta} veces</p>
<button onClick={() => setCuenta(cuenta + 1)}>Incrementar</button>
</div>
)
}
export default Contador
Vamos a desglosar la línea clave:
// valor actual función para actualizar valor inicial
const [cuenta, setCuenta] = useState(0)
useState(0) crea una variable de estado con valor inicial 0. Devuelve un array con dos elementos que desestructuramos: el valor actual (cuenta) y la función para actualizarlo (setCuenta).
La convención de nombres es [algo, setAlgo] — siempre con el prefijo set para la función de actualización.
¿Por qué no usar una variable normal?
Podrías pensar: "¿Y si uso un let normal?" Veamos qué pasa:
// ❌ Esto NO funciona
function Contador() {
let cuenta = 0
function incrementar() {
cuenta++
console.log(cuenta) // El valor SÍ cambia en la consola
// Pero la interfaz NO se actualiza
}
return (
<div>
<p>Has pulsado {cuenta} veces</p>
<button onClick={incrementar}>Incrementar</button>
</div>
)
}
El problema es que React no sabe que el dato cambió. Sin useState, React no tiene razón para volver a renderizar el componente. setCuenta hace dos cosas: actualiza el valor y le dice a React que vuelva a renderizar.
Estado con diferentes tipos de datos
El estado puede ser de cualquier tipo — no solo números:
import { useState } from 'react'
function Perfil() {
const [nombre, setNombre] = useState('Astronauta')
const [activo, setActivo] = useState(true)
const [pedidos, setPedidos] = useState(['Café Nebulosa', 'Latte Cósmico'])
return (
<div>
<h2>Perfil de {nombre}</h2>
<p>Estado: {activo ? 'Activo' : 'Inactivo'}</p>
<p>Pedidos: {pedidos.length}</p>
<input
value={nombre}
onChange={(e) => setNombre(e.target.value)}
/>
<button onClick={() => setActivo(!activo)}>
{activo ? 'Desactivar' : 'Activar'}
</button>
</div>
)
}
export default Perfil
Inmutabilidad: nunca mutes el estado directamente
Esta es una regla crítica en React: nunca modifiques el estado directamente. Siempre crea una copia nueva.
const [productos, setProductos] = useState(['Café', 'Té'])
// ❌ MAL: mutar el array directamente
productos.push('Chocolate')
setProductos(productos)
// React no detecta el cambio porque es la misma referencia
// ✅ BIEN: crear un array nuevo con spread
setProductos([...productos, 'Chocolate'])
// ✅ BIEN: filtrar crea un array nuevo
setProductos(productos.filter(p => p !== 'Té'))
¿Por qué? Porque React compara el estado anterior con el nuevo por referencia. Si le pasas el mismo array (aunque tenga elementos nuevos), React piensa que nada cambió. Creando un array nuevo con el spread operator (...), le das una referencia diferente y React sabe que debe actualizar.
Lo mismo aplica para objetos:
const [usuario, setUsuario] = useState({ nombre: 'Ana', edad: 28 })
// ❌ MAL: mutar el objeto directamente
usuario.nombre = 'Carlos'
setUsuario(usuario)
// ✅ BIEN: crear un objeto nuevo con spread
setUsuario({ ...usuario, nombre: 'Carlos' })
Actualización basada en el estado anterior
Cuando el nuevo estado depende del anterior, usa la forma con función en vez de pasar el valor directamente:
// ⚠️ Funciona, pero puede dar problemas con actualizaciones rápidas
setCuenta(cuenta + 1)
// ✅ Forma segura: función que recibe el estado anterior
setCuenta(prev => prev + 1)
La forma con función (prev => prev + 1) garantiza que siempre trabajas con el valor más reciente. Esto importa cuando React agrupa múltiples actualizaciones o cuando llamas a setCuenta varias veces seguidas:
function incrementarTres() {
// ❌ Esto incrementa solo 1, no 3
// React agrupa las tres llamadas con el mismo valor base
setCuenta(cuenta + 1)
setCuenta(cuenta + 1)
setCuenta(cuenta + 1)
// ✅ Esto sí incrementa 3
// Cada función recibe el resultado de la anterior
setCuenta(prev => prev + 1)
setCuenta(prev => prev + 1)
setCuenta(prev => prev + 1)
}
Estado derivado
No todo necesita ser estado. Si un valor se puede calcular a partir del estado existente, no lo guardes en otro useState — simplemente calcúlalo:
import { useState } from 'react'
function Carrito() {
const [items, setItems] = useState([
{ nombre: 'Café Nebulosa', precio: 3.5 },
{ nombre: 'Latte Cósmico', precio: 4.2 },
])
// ✅ Estado derivado: se calcula a partir de items
const total = items.reduce((sum, item) => sum + item.precio, 0)
const cantidad = items.length
function eliminarItem(index) {
setItems(prev => prev.filter((_, i) => i !== index))
}
return (
<div>
<h2>Tu carrito ({cantidad})</h2>
<ul>
{items.map((item, index) => (
<li key={index}>
{item.nombre} — {item.precio.toFixed(2)} €
<button onClick={() => eliminarItem(index)}>×</button>
</li>
))}
</ul>
<p><strong>Total: {total.toFixed(2)} €</strong></p>
</div>
)
}
export default Carrito
Regla de oro: si puedes calcular un valor a partir del estado existente, no crees otro
useState. Menos estado = menos bugs = código más simple.
Múltiples useState en un componente
Un componente puede tener tantos useState como necesite. Cada uno gestiona un dato independiente:
import { useState } from 'react'
function PedidoCafe() {
const [nombre, setNombre] = useState('')
const [tamanio, setTamanio] = useState('mediano')
const [extras, setExtras] = useState([])
const [enviado, setEnviado] = useState(false)
function toggleExtra(extra) {
setExtras(prev =>
prev.includes(extra)
? prev.filter(e => e !== extra)
: [...prev, extra]
)
}
function enviarPedido() {
if (nombre.trim()) {
setEnviado(true)
}
}
if (enviado) {
return (
<div>
<h2>¡Pedido recibido!</h2>
<p>Nombre: {nombre}</p>
<p>Tamaño: {tamanio}</p>
<p>Extras: {extras.length > 0 ? extras.join(', ') : 'Ninguno'}</p>
<button onClick={() => setEnviado(false)}>Nuevo pedido</button>
</div>
)
}
return (
<div>
<h2>Nuevo pedido</h2>
<input
value={nombre}
onChange={(e) => setNombre(e.target.value)}
placeholder="Tu nombre"
/>
<div>
<label>Tamaño: </label>
<select value={tamanio} onChange={(e) => setTamanio(e.target.value)}>
<option value="pequenio">Pequeño</option>
<option value="mediano">Mediano</option>
<option value="grande">Grande</option>
</select>
</div>
<div>
{['Leche extra', 'Canela', 'Chocolate'].map(extra => (
<label key={extra}>
<input
type="checkbox"
checked={extras.includes(extra)}
onChange={() => toggleExtra(extra)}
/>
{extra}
</label>
))}
</div>
<button onClick={enviarPedido}>Enviar pedido</button>
</div>
)
}
export default PedidoCafe
Resumen
useState(valorInicial)crea una variable de estado y devuelve[valor, setValor].- Llamar a
setValoractualiza el estado y provoca un re-render del componente. - Las variables normales (
let) no provocan re-renders — por eso necesitasuseState. - Inmutabilidad: nunca mutes arrays ni objetos directamente. Usa spread (
...),filter,mappara crear copias nuevas. - Usa la forma con función (
prev => prev + 1) cuando el nuevo estado depende del anterior. - El estado derivado se calcula a partir del estado existente — no crees un
useStateextra si puedes calcularlo.
En la siguiente lección aprenderás useEffect — el hook que te permite ejecutar código cuando el componente se monta, cuando cambian ciertos datos, o cuando se desmonta. Es la puerta a operaciones como llamar a APIs o interactuar con el mundo exterior.
Panel de control de la cafetería
Crea un componente Dashboard para gestionar el inventario de Café Estelar:
- Un estado con un array de productos, donde cada producto es un objeto con
nombre,precioystock. Empieza con al menos 3 productos. - Muestra cada producto en una tarjeta con su nombre, precio, stock actual, y dos botones: "Vender" (reduce stock en 1, mínimo 0) y "Reponer" (incrementa stock en 5).
- Debajo de la lista, muestra el estado derivado: total de productos, stock total, producto más caro y cuántos productos tienen stock 0.
- Un campo de texto y un botón para añadir nuevos productos (con nombre y precio). El stock inicial debe ser 10.
Bonus: añade un botón "Reponer todo" que ponga todos los productos con stock 0 a stock de 10.
lightbulb Pistas
Para actualizar un producto específico dentro del array, usa map: setProductos(prev => prev.map((p, i) => i === index ? ...p, stock: p.stock - 1 : p)). Para el estado derivado, usa reduce para el stock total y filter para contar los agotados. Recuerda que el estado derivado no necesita useState — se calcula directamente en el cuerpo de la función.