Tema
Estado con useState
Hasta ahora tus componentes muestran datos fijos. El estado es lo que hace que una interfaz cobre vida: un contador que sube, un menú que se abre, un carrito que se llena. Aprenderás useState, el hook más usado de React, y las reglas para actualizar estado correctamente.
Concepto
El estado es la "memoria" de un componente: datos que cambian con el tiempo y que, al cambiar, deben actualizar la pantalla.
¿Por qué no usar una variable normal?
jsx
function ContadorRoto() {
let clics = 0
return <button onClick={() => { clics++; console.log(clics) }}>Clics: {clics}</button>
// La consola muestra 1, 2, 3... pero la pantalla sigue en 0
}Dos problemas: React no sabe que la variable cambió (no vuelve a dibujar) y, aunque lo hiciera, la función se ejecuta de nuevo y clics vuelve a ser 0.
useState resuelve ambos: guarda el valor entre renders y avisa a React que debe volver a dibujar.
jsx
import { useState } from 'react'
function Contador() {
// [valorActual, funciónParaCambiarlo] = useState(valorInicial)
const [clics, setClics] = useState(0)
return <button onClick={() => setClics(clics + 1)}>Clics: {clics}</button>
}Analogía: el estado es una pizarra que React guarda por ti. Tú no escribes directamente en ella; le pides a React "cambia esto" (setClics) y él borra, escribe el nuevo valor y vuelve a dibujar la pantalla.
¿Qué pasa al llamar a setClics?
- React guarda el nuevo valor.
- Programa un nuevo render: vuelve a ejecutar la función
Contador. - En ese render,
useStatedevuelve el valor nuevo. - React compara el JSX nuevo con el anterior y actualiza el DOM.
Reglas de los hooks
Los hooks (funciones que empiezan con use) tienen dos reglas:
- Llámalos sólo en el nivel superior del componente: nunca dentro de
if,foro funciones anidadas. - Llámalos sólo en componentes o en custom hooks.
jsx
function Malo({ activo }) {
if (activo) {
const [x, setX] = useState(0) // ❌ hook dentro de un if
}
}React identifica cada estado por el orden en que se llaman los hooks; si el orden cambia entre renders, se confunde.
Varios estados y distintos tipos
jsx
function Formulario() {
const [nombre, setNombre] = useState('') // string
const [edad, setEdad] = useState(18) // número
const [acepta, setAcepta] = useState(false) // booleano
const [etiquetas, setEtiquetas] = useState([]) // arreglo
const [direccion, setDireccion] = useState({ calle: '', ciudad: '' }) // objeto
// ...
}Actualizar con la forma funcional
Si el nuevo valor depende del anterior, pasa una función:
jsx
function Contador() {
const [n, setN] = useState(0)
function sumarTres() {
// ❌ Las tres usan el mismo "n" de este render: resultado +1
// setN(n + 1); setN(n + 1); setN(n + 1)
// ✅ Cada una recibe el valor más reciente: resultado +3
setN((anterior) => anterior + 1)
setN((anterior) => anterior + 1)
setN((anterior) => anterior + 1)
}
return <button onClick={sumarTres}>{n}</button>
}El estado dentro de un render es como una foto: n no cambia hasta el siguiente render. La forma funcional siempre trabaja con el valor actualizado.
Inmutabilidad: objetos y arreglos
React detecta cambios comparando referencias. Si modificas un objeto o arreglo directamente, la referencia es la misma y React cree que nada cambió.
Regla: nunca mutes el estado; crea una copia nueva.
Objetos
jsx
const [usuario, setUsuario] = useState({ nombre: 'Ana', email: 'ana@correo.com', edad: 28 })
// ❌ Mutación: la pantalla no se actualiza
usuario.edad = 29
setUsuario(usuario)
// ✅ Copia con spread y sobrescribe lo que cambia
setUsuario({ ...usuario, edad: 29 })
// ✅ Objetos anidados: copia cada nivel
const [perfil, setPerfil] = useState({ nombre: 'Ana', direccion: { ciudad: 'CDMX', cp: '01000' } })
setPerfil({ ...perfil, direccion: { ...perfil.direccion, ciudad: 'Puebla' } })Arreglos
| Operación | ❌ Muta | ✅ Copia nueva |
|---|---|---|
| Agregar | push, unshift | [...arr, nuevo], [nuevo, ...arr] |
| Eliminar | splice, pop | arr.filter(x => x.id !== id) |
| Reemplazar | arr[i] = x | arr.map(x => x.id === id ? {...x, ...cambios} : x) |
| Ordenar | sort, reverse | arr.toSorted(), arr.toReversed() o [...arr].sort() |
jsx
const [tareas, setTareas] = useState([
{ id: 1, texto: 'Aprender useState', hecha: false }
])
// Agregar
setTareas([...tareas, { id: Date.now(), texto: 'Nueva', hecha: false }])
// Eliminar
setTareas(tareas.filter((t) => t.id !== 1))
// Actualizar uno
setTareas(tareas.map((t) => (t.id === 1 ? { ...t, hecha: !t.hecha } : t)))Estado inicial costoso
Si calcular el valor inicial es costoso (por ejemplo, leer localStorage), pasa una función: sólo se ejecuta en el primer render.
jsx
// ❌ Se lee localStorage en CADA render
const [notas, setNotas] = useState(JSON.parse(localStorage.getItem('notas') ?? '[]'))
// ✅ Sólo la primera vez
const [notas, setNotas] = useState(() => JSON.parse(localStorage.getItem('notas') ?? '[]'))Estado derivado: no guardes lo que puedes calcular
jsx
function Carrito() {
const [items, setItems] = useState([
{ id: 1, nombre: 'Playera', precio: 199, cantidad: 2 },
{ id: 2, nombre: 'Gorra', precio: 249, cantidad: 1 }
])
// ❌ No hagas const [total, setTotal] = useState(0) y lo sincronices a mano
// ✅ Calcúlalo en cada render: siempre estará correcto
const total = items.reduce((suma, i) => suma + i.precio * i.cantidad, 0)
const cantidadTotal = items.reduce((suma, i) => suma + i.cantidad, 0)
return (
<p>
{cantidadTotal} artículos · Total: ${total}
</p>
)
}Caso real: carrito con cantidades
jsx
import { useState } from 'react'
const catalogo = [
{ id: 1, nombre: 'Playera', precio: 199 },
{ id: 2, nombre: 'Gorra', precio: 249 },
{ id: 3, nombre: 'Sudadera', precio: 599 }
]
export default function App() {
const [carrito, setCarrito] = useState([]) // [{ id, nombre, precio, cantidad }]
function agregar(producto) {
setCarrito((actual) => {
const existe = actual.find((i) => i.id === producto.id)
if (existe) {
return actual.map((i) => (i.id === producto.id ? { ...i, cantidad: i.cantidad + 1 } : i))
}
return [...actual, { ...producto, cantidad: 1 }]
})
}
function cambiarCantidad(id, delta) {
setCarrito((actual) =>
actual
.map((i) => (i.id === id ? { ...i, cantidad: i.cantidad + delta } : i))
.filter((i) => i.cantidad > 0) // si llega a 0, se elimina
)
}
const total = carrito.reduce((s, i) => s + i.precio * i.cantidad, 0)
return (
<main style={{ maxWidth: 480, margin: '2rem auto', fontFamily: 'system-ui' }}>
<h2>Productos</h2>
{catalogo.map((p) => (
<div key={p.id} style={{ display: 'flex', justifyContent: 'space-between', margin: '8px 0' }}>
<span>{p.nombre} — ${p.precio}</span>
<button onClick={() => agregar(p)}>Agregar</button>
</div>
))}
<h2>Carrito</h2>
{carrito.length === 0 && <p>Tu carrito está vacío.</p>}
{carrito.map((i) => (
<div key={i.id} style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
<span style={{ flex: 1 }}>{i.nombre}</span>
<button onClick={() => cambiarCantidad(i.id, -1)}>−</button>
<span>{i.cantidad}</span>
<button onClick={() => cambiarCantidad(i.id, 1)}>+</button>
<span>${i.precio * i.cantidad}</span>
</div>
))}
<h3>Total: ${total}</h3>
</main>
)
}Buena práctica
- Guarda en estado lo mínimo: si algo se puede calcular a partir de otro estado o de las props, calcúlalo.
- Usa la forma funcional
setX(prev => ...)siempre que el nuevo valor dependa del anterior.
Error común
Esperar que el estado cambie inmediatamente después del set:
jsx
setClics(clics + 1)
console.log(clics) // sigue mostrando el valor ANTERIOREl nuevo valor estará disponible en el próximo render.
Ejercicios
- Crea un componente
Interruptorque alterne entre "Modo claro" y "Modo oscuro", cambiando el fondo de la página con un estado booleano. - Crea una lista de invitados: un input, un botón "Agregar" y una lista donde cada invitado tenga un botón "Quitar". No permitas nombres vacíos ni repetidos.
Ver solución
jsx
// 1
import { useState } from 'react'
export function Interruptor() {
const [oscuro, setOscuro] = useState(false)
return (
<div style={{
minHeight: '100vh',
background: oscuro ? '#111' : '#fff',
color: oscuro ? '#fff' : '#111',
padding: 24
}}>
<button onClick={() => setOscuro((o) => !o)}>
{oscuro ? '☀️ Modo claro' : '🌙 Modo oscuro'}
</button>
</div>
)
}jsx
// 2
import { useState } from 'react'
export function Invitados() {
const [nombre, setNombre] = useState('')
const [invitados, setInvitados] = useState([])
const limpio = nombre.trim()
const repetido = invitados.some((i) => i.toLowerCase() === limpio.toLowerCase())
function agregar() {
if (!limpio || repetido) return
setInvitados([...invitados, limpio])
setNombre('')
}
return (
<div>
<input value={nombre} onChange={(e) => setNombre(e.target.value)} placeholder="Nombre" />
<button onClick={agregar} disabled={!limpio || repetido}>Agregar</button>
{repetido && <p style={{ color: 'crimson' }}>Ya está en la lista</p>}
<ul>
{invitados.map((inv) => (
<li key={inv}>
{inv}{' '}
<button onClick={() => setInvitados(invitados.filter((i) => i !== inv))}>Quitar</button>
</li>
))}
</ul>
<p>Total: {invitados.length}</p>
</div>
)
}Resumen
const [valor, setValor] = useState(inicial)guarda datos entre renders y vuelve a dibujar al cambiar.- Llama a los hooks en el nivel superior, nunca dentro de condiciones o bucles.
- Usa
setX(prev => ...)cuando el nuevo valor dependa del anterior. - Nunca mutes objetos ni arreglos del estado: crea copias con spread,
mapyfilter. - No guardes en estado lo que puedes calcular.