Skip to content

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? ​

  1. React guarda el nuevo valor.
  2. Programa un nuevo render: vuelve a ejecutar la función Contador.
  3. En ese render, useState devuelve el valor nuevo.
  4. 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:

  1. Llámalos sólo en el nivel superior del componente: nunca dentro de if, for o funciones anidadas.
  2. 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
Agregarpush, unshift[...arr, nuevo], [nuevo, ...arr]
Eliminarsplice, poparr.filter(x => x.id !== id)
Reemplazararr[i] = xarr.map(x => x.id === id ? {...x, ...cambios} : x)
Ordenarsort, reversearr.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 ANTERIOR

El nuevo valor estará disponible en el próximo render.

Ejercicios ​

  1. Crea un componente Interruptor que alterne entre "Modo claro" y "Modo oscuro", cambiando el fondo de la página con un estado booleano.
  2. 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, map y filter.
  • No guardes en estado lo que puedes calcular.

Academia Web — aprende, copia y construye.