← React & TypeScript

Hooks

¿Cómo funciona useState? Si llamo a la función set, ¿el valor cambia inmediatamente?

Ver respuesta — intenta responderla en voz alta primero

useState le pide a React que guarde un valor entre renders y te da una función para actualizarlo. La función set NO cambia el valor de inmediato: programa un nuevo render, y es en ese siguiente render donde la variable ya tiene el valor nuevo.

Un componente de función se ejecuta completo en cada render. Las variables locales normales se pierden entre ejecuciones, así que React necesita un lugar externo para guardar el estado. Eso es lo que hace useState.

Los puntos clave:

  • El estado es una foto por render. Dentro de un render, la variable de estado es una constante. Aunque llames a setContador(5), la variable contador de ese render sigue valiendo lo que valía.
  • El set programa un render, no muta. React agrupa (hace batching de) varias actualizaciones y vuelve a ejecutar el componente con el valor nuevo.
  • Forma funcional: si el valor nuevo depende del anterior, usa setContador(prev => prev + 1). Así React te pasa el valor más reciente y evitas trabajar con una foto vieja.
  • Objetos y arreglos son inmutables en la práctica. No modifiques el objeto existente: crea uno nuevo con spread ({ ...persona, edad: 30 }). Si mutas, React puede no detectar el cambio porque compara referencias.
  • Inicialización perezosa: si el valor inicial es caro de calcular, pasa una función useState(() => calcularCaro()). React solo la ejecuta en el primer render.
import { useState } from "react";

function Contador() {
  // Inicialización perezosa: la función solo corre en el primer render
  const [contador, setContador] = useState<number>(() => {
    const guardado = localStorage.getItem("contador");
    return guardado ? Number(guardado) : 0;
  });

  const [tags, setTags] = useState<string[]>([]);

  function sumarTres() {
    // Forma funcional: cada llamada recibe el valor más reciente
    setContador(prev => prev + 1);
    setContador(prev => prev + 1);
    setContador(prev => prev + 1);
    console.log(contador); // imprime el valor ANTES de sumar: el set no es inmediato
  }

  function agregarTag(tag: string) {
    // Inmutabilidad: creamos un arreglo nuevo, no hacemos tags.push(tag)
    setTags(prev => [...prev, tag]);
  }

  return (
    <div>
      <p>Valor: {contador}</p>
      <button onClick={sumarTres}>+3</button>
      <button onClick={() => agregarTag("react")}>Agregar tag</button>
      <ul>{tags.map(t => <li key={t}>{t}</li>)}</ul>
    </div>
  );
}

Llamar al set varias veces usando la variable del render en lugar de la forma funcional:

// MAL: las tres llamadas usan la misma foto (contador = 0)
setContador(contador + 1);
setContador(contador + 1);
setContador(contador + 1);
// Resultado: el contador termina en 1, no en 3

// BIEN: la forma funcional encadena las actualizaciones
setContador(prev => prev + 1);
setContador(prev => prev + 1);
setContador(prev => prev + 1);
// Resultado: el contador termina en 3

El mismo error aparece con objetos: mutar con persona.edad = 30 y luego setPersona(persona) no re-renderiza, porque la referencia es la misma. La corrección es setPersona({ ...persona, edad: 30 }).

useState guarda un valor que sobrevive entre renders y me da una función para actualizarlo. Lo importante es que el set no es inmediato: programa un nuevo render y el valor nuevo aparece hasta esa siguiente ejecución, porque dentro de un render el estado es una foto fija. Cuando el valor nuevo depende del anterior, uso la forma funcional setX(prev => ...) para no trabajar con datos viejos. Con objetos y arreglos nunca muto: creo copias nuevas con spread para que React detecte el cambio. Y si el valor inicial es caro de calcular, paso una función para que solo se ejecute en el primer render.

Reto rápido

Crea un componente ListaCompras con un estado productos: string[] y un input controlado. Al presionar "Agregar", el producto se añade a la lista sin mutar el arreglo, y el input se limpia.

Ver solución
import { useState } from "react";

function ListaCompras() {
  const [productos, setProductos] = useState<string[]>([]);
  const [texto, setTexto] = useState("");

  function agregar() {
    if (texto.trim() === "") return;
    // Copia nueva del arreglo: nada de productos.push(texto)
    setProductos(prev => [...prev, texto.trim()]);
    setTexto(""); // limpiamos el input
  }

  return (
    <div>
      <input value={texto} onChange={e => setTexto(e.target.value)} />
      <button onClick={agregar}>Agregar</button>
      <ul>{productos.map((p, i) => <li key={`${p}-${i}`}>{p}</li>)}</ul>
    </div>
  );
}