En esta página

useState y Eventos

15 min lectura TextoCap. 2 — Estado y efectos

Estado en React — el concepto central

El estado es la información que un componente gestiona internamente y que puede cambiar con el tiempo. Cuando el estado cambia, React re-renderiza el componente automáticamente para reflejar el nuevo valor en la UI.

Sin estado, los componentes son estáticos (puramente presentacionales). Con estado, se vuelven interactivos y dinámicos.

import { useState } from 'react';

function Contador(): React.JSX.Element {
  // Declaración: [valor actual, función para actualizar]
  const [conteo, setConteo] = useState<number>(0);

  return (
    <div>
      <p>Conteo: {conteo}</p>
      <button type="button" onClick={() => setConteo(conteo + 1)}>
        Incrementar
      </button>
    </div>
  );
}

useState retorna una tupla: el valor actual y una función setter. Cuando llamas al setter, React programa un re-render con el nuevo valor.

Tipado de useState con TypeScript

TypeScript puede inferir el tipo del estado desde el valor inicial, pero a veces necesitas ser explícito:

// Inferencia automática (número)
const [edad, setEdad] = useState(25);

// Explícito cuando el valor inicial es null/undefined
const [usuario, setUsuario] = useState<Usuario | null>(null);

// Arrays tipados
const [elementos, setElementos] = useState<string[]>([]);

// Union types
const [estado, setEstado] = useState<'cargando' | 'exitoso' | 'error'>('cargando');

El principio de inmutabilidad

El error más común con useState es mutar el estado directamente. React usa comparación por referencia para detectar cambios: si el objeto o array es el mismo en memoria, React asume que no hubo cambios y no re-renderiza.

// ❌ INCORRECTO — mutación directa
const agregarItem = () => {
  items.push('nuevo'); // Muta el array original
  setItems(items);     // React ve el mismo array → no re-renderiza
};

// ✅ CORRECTO — crear nuevo array
const agregarItem = () => {
  setItems([...items, 'nuevo']); // Nuevo array en memoria
};

// ❌ INCORRECTO — mutar objeto
const actualizar = () => {
  usuario.nombre = 'Ana'; // Muta el objeto original
  setUsuario(usuario);    // Misma referencia → no re-renderiza
};

// ✅ CORRECTO — crear nuevo objeto
const actualizar = () => {
  setUsuario({ ...usuario, nombre: 'Ana' }); // Nuevo objeto
};

La función actualizadora

Cuando el nuevo estado depende del anterior, usa la forma funcional del setter:

// ❌ Puede fallar si hay múltiples actualizaciones en el mismo ciclo
setConteo(conteo + 1);
setConteo(conteo + 1); // Ambas usan el mismo `conteo` capturado

// ✅ Siempre usa el valor más reciente del estado
setConteo((prev) => prev + 1);
setConteo((prev) => prev + 1); // Cada una recibe el prev actualizado

Manejo de eventos

React usa un sistema de eventos sintéticos (SyntheticEvent) que envuelve los eventos nativos del navegador para garantizar compatibilidad cross-browser.

Eventos de ratón

import { type React } from 'react';

function AreaInteractiva(): React.JSX.Element {
  const manejarClic = (e: React.MouseEvent<HTMLDivElement>) => {
    console.log(`Clic en: ${e.clientX}, ${e.clientY}`);
    e.stopPropagation(); // Detener propagación
  };

  const manejarDoble = () => {
    console.log('Doble clic');
  };

  return (
    <div
      onClick={manejarClic}
      onDoubleClick={manejarDoble}
      onMouseEnter={() => console.log('Entró')}
      onMouseLeave={() => console.log('Salió')}
    >
      Interactúa conmigo
    </div>
  );
}

Eventos de teclado

function BuscadorRapido(): React.JSX.Element {
  const [busqueda, setBusqueda] = useState('');

  const manejarTecla = (e: React.KeyboardEvent<HTMLInputElement>) => {
    if (e.key === 'Enter') {
      console.log('Buscar:', busqueda);
    }
    if (e.key === 'Escape') {
      setBusqueda('');
    }
  };

  return (
    <input
      type="search"
      value={busqueda}
      onChange={(e) => setBusqueda(e.target.value)}
      onKeyDown={manejarTecla}
      placeholder="Buscar… (Enter para buscar, Esc para limpiar)"
    />
  );
}

Eventos de formulario

function FormularioContacto(): React.JSX.Element {
  const [datos, setDatos] = useState({ nombre: '', mensaje: '' });

  const manejarSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault(); // Evita recarga de página
    console.log('Enviando:', datos);
  };

  const manejarCambio = (
    e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
  ) => {
    const { name, value } = e.target;
    setDatos((prev) => ({ ...prev, [name]: value }));
  };

  return (
    <form onSubmit={manejarSubmit}>
      <input
        name="nombre"
        value={datos.nombre}
        onChange={manejarCambio}
        required
      />
      <textarea
        name="mensaje"
        value={datos.mensaje}
        onChange={manejarCambio}
        required
      />
      <button type="submit">Enviar</button>
    </form>
  );
}

Múltiples estados vs un objeto de estado

Una duda frecuente: ¿usar múltiples useState independientes o un solo objeto?

// Opción A: múltiples estados independientes (preferida para valores no relacionados)
const [nombre, setNombre] = useState('');
const [email, setEmail] = useState('');
const [edad, setEdad] = useState(0);

// Opción B: objeto (preferida cuando los campos siempre cambian juntos)
const [perfil, setPerfil] = useState({ nombre: '', email: '', edad: 0 });

Guía práctica: agrupa el estado que siempre se actualiza junto (como los campos de un formulario) y mantén separado el estado que cambia de forma independiente.

Estado elevado (Lifting State Up)

Cuando dos componentes hermanos necesitan compartir estado, el estado se mueve al ancestro común más cercano:

function App(): React.JSX.Element {
  // Estado elevado al padre para compartir entre hijos
  const [temperatura, setTemperatura] = useState(20);

  return (
    <div>
      <SensorCelsius valor={temperatura} onChange={setTemperatura} />
      <SensorFahrenheit
        valor={(temperatura * 9) / 5 + 32}
        onChange={(f) => setTemperatura(((f - 32) * 5) / 9)}
      />
    </div>
  );
}

Este patrón es fundamental en React: el estado fluye hacia abajo mediante props, y los eventos fluyen hacia arriba mediante callbacks.

Nunca mutes el estado directamente
Escribir state.propiedad = valor o array.push(item) no provoca re-renders porque React no detecta mutaciones directas. Siempre usa setEstado() con un nuevo objeto o array. El patrón correcto para objetos es spread (...prev) y para arrays usa map, filter o spread.
La función actualizadora garantiza el estado más reciente
Cuando el nuevo estado depende del anterior, usa la forma funcional: setContador(prev => prev + 1). Esto evita el problema de closures estales cuando múltiples actualizaciones ocurren en el mismo ciclo de render, especialmente en event handlers asíncronos.
Tipo de eventos en TypeScript
El tipo correcto para onChange de input es React.ChangeEvent<HTMLInputElement>. Para onClick es React.MouseEvent<HTMLButtonElement>. Para onSubmit es React.FormEvent<HTMLFormElement>. TypeScript los infiere automáticamente cuando la función se define inline.