useState y useEffect explicados con ejemplos reales

D
DanisCh
(Actualizado: ) • 13 min de lectura
useState y useEffect explicados con ejemplos reales
HTML JavaScript

Los hooks son funciones especiales de React que permiten añadir estado y otras capacidades a los componentes de función. Antes de que existieran los hooks (React 16.8, 2019), para tener estado o efectos secundarios necesitabas clases. Hoy, casi todo el código React moderno se escribe con hooks. Los dos más fundamentales son useState para gestionar estado y useEffect para sincronizar el componente con el mundo exterior.

En esta guía aprenderás cómo funcionan en profundidad, cuáles son los errores más habituales y cómo usarlos en situaciones reales.

useState en profundidad

useState añade una variable de estado a un componente. Cuando esa variable cambia, React re-renderiza el componente con el nuevo valor.

import { useState } from 'react';

// Sintaxis básica
const [valor, setValor] = useState(valorInicial);
// valor    → el valor actual del estado
// setValor → función para actualizarlo (desencadena un re-render)
// valorInicial → el valor al montar el componente (solo se usa la primera vez)

Tipos de estado habituales

// Estado primitivo
const [contador, setContador]     = useState(0);
const [nombre, setNombre]         = useState('');
const [activo, setActivo]         = useState(false);
const [precio, setPrecio]         = useState(null);

// Estado de objeto
const [usuario, setUsuario] = useState({
  nombre: '',
  email:  '',
  rol:    'visitante'
});

// Estado de array
const [tareas, setTareas] = useState([]);

// Estado para la UI
const [cargando, setCargando]   = useState(false);
const [error, setError]         = useState(null);
const [modal, setModal]         = useState(false);

Actualizar el estado correctamente

// ─── Primitivos: directo ──────────────────────────────────────────────────────
const [contador, setContador] = useState(0);

setContador(5);              // asignar un valor nuevo
setContador(contador + 1);   // basado en el valor actual

// Forma funcional: más segura cuando el nuevo valor depende del anterior
// React puede agrupar varios setStates y la forma funcional garantiza
// que siempre usas el valor más reciente
setContador(prev => prev + 1);


// ─── Objetos: nunca mutarlos, siempre crear uno nuevo ─────────────────────────
const [usuario, setUsuario] = useState({ nombre: 'Ana', email: 'ana@mail.com' });

// ❌ Mutación directa: React no detecta el cambio
usuario.nombre = 'Carlos';
setUsuario(usuario);   // mismo objeto → React no re-renderiza

// ✅ Crear nuevo objeto con spread
setUsuario({ ...usuario, nombre: 'Carlos' });

// Actualizar campo dinámico (útil en formularios)
function actualizarCampo(campo, valor) {
  setUsuario(prev => ({ ...prev, [campo]: valor }));
}


// ─── Arrays: siempre crear uno nuevo ──────────────────────────────────────────
const [items, setItems] = useState([1, 2, 3]);

// Añadir elemento
setItems([...items, 4]);
setItems(prev => [...prev, 4]);

// Eliminar elemento
setItems(items.filter(item => item !== 2));

// Actualizar un elemento
setItems(items.map(item => item === 2 ? 99 : item));

// Reordenar (ejemplo: mover el primer elemento al final)
const [primero, ...resto] = items;
setItems([...resto, primero]);

Estado inicial perezoso (lazy initialization)

// Si el valor inicial requiere un cálculo costoso, pasa una función
// React solo la ejecuta la primera vez, no en cada re-render

// ❌ Se ejecuta en cada render aunque solo se use la primera vez
const [datos, setDatos] = useState(calcularDatosComplejos());

// ✅ Se ejecuta solo una vez al montar el componente
const [datos, setDatos] = useState(() => calcularDatosComplejos());

// Ejemplo real: leer de localStorage
const [tema, setTema] = useState(() => {
  return localStorage.getItem('tema') || 'claro';
});

Ejemplo real: formulario controlado

import { useState } from 'react';

function FormularioRegistro() {
  const [campos, setCampos] = useState({
    nombre:     '',
    email:      '',
    contraseña: '',
  });
  const [errores, setErrores] = useState({});
  const [enviado, setEnviado] = useState(false);

  function actualizarCampo(e) {
    const { name, value } = e.target;
    setCampos(prev => ({ ...prev, [name]: value }));

    // Limpiar el error del campo al escribir
    if (errores[name]) {
      setErrores(prev => ({ ...prev, [name]: null }));
    }
  }

  function validar() {
    const nuevosErrores = {};
    if (!campos.nombre.trim())
      nuevosErrores.nombre = 'El nombre es obligatorio';
    if (!campos.email.includes('@'))
      nuevosErrores.email = 'El email no es válido';
    if (campos.contraseña.length < 8)
      nuevosErrores.contraseña = 'Mínimo 8 caracteres';
    return nuevosErrores;
  }

  async function manejarEnvio(e) {
    e.preventDefault();
    const erroresEncontrados = validar();

    if (Object.keys(erroresEncontrados).length > 0) {
      setErrores(erroresEncontrados);
      return;
    }

    // Aquí iría la llamada a la API
    setEnviado(true);
  }

  if (enviado) {
    return <p>¡Registro completado! Revisa tu email.</p>;
  }

  return (
    <form onSubmit={manejarEnvio}>
      <div>
        <input
          name="nombre"
          value={campos.nombre}
          onChange={actualizarCampo}
          placeholder="Tu nombre"
        />
        {errores.nombre && <span className="error">{errores.nombre}</span>}
      </div>
      <div>
        <input
          name="email"
          type="email"
          value={campos.email}
          onChange={actualizarCampo}
          placeholder="tu@email.com"
        />
        {errores.email && <span className="error">{errores.email}</span>}
      </div>
      <div>
        <input
          name="contraseña"
          type="password"
          value={campos.contraseña}
          onChange={actualizarCampo}
          placeholder="Contraseña"
        />
        {errores.contraseña && <span className="error">{errores.contraseña}</span>}
      </div>
      <button type="submit">Registrarse</button>
    </form>
  );
}

useEffect en profundidad

useEffect permite sincronizar tu componente con algo externo: una API, el DOM, un temporizador, una suscripción. Es el lugar correcto para hacer cosas que no son renderizado puro.

import { useEffect } from 'react';

useEffect(
  () => {
    // Código del efecto: se ejecuta después de renderizar
    // Aquí puedes hacer peticiones, suscribirte a eventos, etc.

    return () => {
      // Función de limpieza (opcional):
      // se ejecuta antes de que el efecto se vuelva a ejecutar
      // y cuando el componente se desmonta
    };
  },
  [dependencia1, dependencia2]  // array de dependencias
);

El array de dependencias: la clave de useEffect

// 1. Sin array: se ejecuta después de CADA render
useEffect(() => {
  console.log('Después de cada render');
});
// Rara vez lo quieres así: puede causar bucles infinitos fácilmente

// 2. Array vacío []: se ejecuta solo al montar el componente
useEffect(() => {
  console.log('Solo al montar (equivale a componentDidMount)');
  // Ideal para: cargar datos iniciales, suscripciones únicas
}, []);

// 3. Con dependencias: se ejecuta cuando alguna dependencia cambia
useEffect(() => {
  console.log('usuarioId cambió a:', usuarioId);
  // Ideal para: recargar datos cuando un prop/estado cambia
}, [usuarioId]);

// La regla: incluye en el array todo valor del componente
// que uses dentro del efecto (props, estado, funciones)
// Si lo omites, el efecto puede usar valores "rancios" (stale closure)

Ejemplo 1: cargar datos de una API

import { useState, useEffect } from 'react';

function PerfilUsuario({ usuarioId }) {
  const [usuario, setUsuario]   = useState(null);
  const [cargando, setCargando] = useState(true);
  const [error, setError]       = useState(null);

  useEffect(() => {
    // Resetear estado al cambiar de usuario
    setCargando(true);
    setError(null);
    setUsuario(null);

    // Variable para evitar actualizar el estado si el componente se desmontó
    let cancelado = false;

    async function cargarUsuario() {
      try {
        const respuesta = await fetch(`/api/usuarios/${usuarioId}`);

        if (!respuesta.ok) {
          throw new Error(`HTTP ${respuesta.status}`);
        }

        const datos = await respuesta.json();

        if (!cancelado) {
          setUsuario(datos);
        }
      } catch (err) {
        if (!cancelado) {
          setError(err.message);
        }
      } finally {
        if (!cancelado) {
          setCargando(false);
        }
      }
    }

    cargarUsuario();

    // Función de limpieza: si el componente se desmonta o usuarioId cambia
    // antes de que termine la petición, cancelamos la actualización del estado
    return () => {
      cancelado = true;
    };
  }, [usuarioId]);   // se vuelve a ejecutar cada vez que usuarioId cambia

  if (cargando) return <p>Cargando...</p>;
  if (error)    return <p>Error: {error}</p>;
  if (!usuario) return null;

  return (
    <div>
      <h2>{usuario.nombre}</h2>
      <p>{usuario.email}</p>
    </div>
  );
}

Ejemplo 2: suscripción a un evento del DOM

import { useState, useEffect } from 'react';

function PosicionRaton() {
  const [posicion, setPosicion] = useState({ x: 0, y: 0 });

  useEffect(() => {
    function actualizarPosicion(evento) {
      setPosicion({ x: evento.clientX, y: evento.clientY });
    }

    // Suscribirse al evento al montar
    window.addEventListener('mousemove', actualizarPosicion);

    // Limpiar al desmontar: FUNDAMENTAL para evitar fugas de memoria
    return () => {
      window.removeEventListener('mousemove', actualizarPosicion);
    };
  }, []);   // [] porque solo queremos suscribirnos una vez

  return (
    <p>Ratón en: X={posicion.x}, Y={posicion.y}</p>
  );
}

Ejemplo 3: temporizador

import { useState, useEffect } from 'react';

function Cronometro() {
  const [segundos, setSegundos] = useState(0);
  const [activo, setActivo]     = useState(false);

  useEffect(() => {
    if (!activo) return;   // si no está activo, no hacer nada

    const intervalo = setInterval(() => {
      setSegundos(prev => prev + 1);   // forma funcional: más segura en setInterval
    }, 1000);

    // Limpiar el intervalo cuando activo cambia o el componente se desmonta
    return () => clearInterval(intervalo);
  }, [activo]);   // se vuelve a ejecutar cuando activo cambia

  function formatearTiempo(seg) {
    const m = Math.floor(seg / 60).toString().padStart(2, '0');
    const s = (seg % 60).toString().padStart(2, '0');
    return `${m}:${s}`;
  }

  return (
    <div>
      <p className="tiempo">{formatearTiempo(segundos)}</p>
      <button onClick={() => setActivo(!activo)}>
        {activo ? 'Pausar' : 'Iniciar'}
      </button>
      <button onClick={() => { setActivo(false); setSegundos(0); }}>
        Resetear
      </button>
    </div>
  );
}

Ejemplo 4: sincronizar con localStorage

import { useState, useEffect } from 'react'; function useLocalStorage(clave, valorInicial) {  // Inicializar desde localStorage (lazy initialization)  const [valor, setValor] = useState(() => {    try {      const guardado = localStorage.getItem(clave);      return guardado ? JSON.parse(guardado) : valorInicial;    } catch {      return valorInicial;    }  });  // Sincronizar con localStorage cuando el valor cambia  useEffect(() => {    try {      localStorage.setItem(clave, JSON.stringify(valor));    } catch {      // localStorage puede fallar en modo privado o si está lleno    }  }, [clave, valor]);  return [valor, setValor]; } // Usar el hook personalizado igual que useState function Preferencias() {  const [tema, setTema]   = useLocalStorage('tema', 'claro');  const [idioma, setIdioma] = useLocalStorage('idioma', 'es');  return (    <div>      <select value={tema} onChange={e => setTema(e.target.value)}>        <option value="claro">Claro</option>        <option value="oscuro">Oscuro</option>      </select>      {/* Los cambios se guardan en localStorage automáticamente */}    </div>  ); }

Ejemplo 5: título del documento y cleanup

import { useState, useEffect } from 'react'; function Chat({ contacto }) {  const [mensajes, setMensajes] = useState([]);  const [noLeidos, setNoLeidos]  = useState(0);  // Actualizar el título de la pestaña con los mensajes no leídos  useEffect(() => {    if (noLeidos > 0) {      document.title = `(${noLeidos}) Chat con ${contacto.nombre}`;    } else {      document.title = `Chat con ${contacto.nombre}`;    }    // Al desmontar el componente, restaurar el título original    return () => {      document.title = 'Mi aplicación';    };  }, [noLeidos, contacto.nombre]);  // Suscribirse a mensajes nuevos (WebSocket simulado)  useEffect(() => {    const conexion = crearConexionChat(contacto.id);    conexion.onMensaje = (mensaje) => {      setMensajes(prev => [...prev, mensaje]);      setNoLeidos(prev => prev + 1);    };    conexion.conectar();    return () => {      conexion.desconectar();   // cerrar la conexión al desmontar    };  }, [contacto.id]);   // nueva conexión si cambia el contacto  return (    <div>      <h2>{contacto.nombre} {noLeidos > 0 && <span>({noLeidos} nuevos)</span>}</h2>      <div className="mensajes">        {mensajes.map((m, i) => <p key={i}>{m.texto}</p>)}      </div>      <button onClick={() => setNoLeidos(0)}>Marcar como leídos</button>    </div>  ); }

Los errores más comunes con useEffect

Dependencias faltantes (stale closure)

const [contador, setContador] = useState(0);

// ❌ contador no está en las dependencias: el efecto captura el valor inicial (0)
// y nunca lo actualiza aunque contador cambie en el estado
useEffect(() => {
  const intervalo = setInterval(() => {
    console.log('Contador:', contador);   // siempre imprime 0
  }, 1000);
  return () => clearInterval(intervalo);
}, []);   // ← falta contador aquí

// ✅ Opción 1: añadir contador a las dependencias
useEffect(() => {
  const intervalo = setInterval(() => {
    console.log('Contador:', contador);
  }, 1000);
  return () => clearInterval(intervalo);
}, [contador]);   // se reinicia el intervalo cuando contador cambia

// ✅ Opción 2: usar la forma funcional del setter (no necesita leer el estado)
useEffect(() => {
  const intervalo = setInterval(() => {
    setContador(prev => {
      console.log('Contador:', prev);
      return prev;   // no cambiamos el valor, solo lo leemos
    });
  }, 1000);
  return () => clearInterval(intervalo);
}, []);   // [] es correcto aquí porque no dependemos de contador directamente

Bucle infinito

// ❌ Bucle infinito: el efecto modifica un estado que está en sus dependencias const [datos, setDatos] = useState([]); useEffect(() => {  setDatos([...datos, 'nuevo']);   // modifica datos }, [datos]);                        // que está en las dependencias → bucle infinito // ✅ No incluir el estado que modificas como dependencia // Si el nuevo valor depende del anterior, usa la forma funcional useEffect(() => {  setDatos(prev => [...prev, 'nuevo']);   // no necesita leer datos }, []);   // [] si solo quieres ejecutarlo una vez

Petición async directamente en useEffect

// ❌ No puedes hacer useEffect async directamente useEffect(async () => {         // ← esto no funciona bien  const datos = await fetch('/api');  setDatos(datos); }, []); // ✅ Define la función async dentro y llámala useEffect(() => {  async function cargar() {    const respuesta = await fetch('/api');    const datos = await respuesta.json();    setDatos(datos);  }  cargar(); }, []);

useState y useEffect juntos: el patrón completo de carga de datos

import { useState, useEffect } from 'react'; // Hook personalizado que encapsula el patrón completo function useDatos(url) {  const [datos, setDatos]       = useState(null);  const [cargando, setCargando] = useState(true);  const [error, setError]       = useState(null);  useEffect(() => {    let cancelado = false;    setCargando(true);    setError(null);    async function cargar() {      try {        const respuesta = await fetch(url);        if (!respuesta.ok) throw new Error(`HTTP ${respuesta.status}`);        const json = await respuesta.json();        if (!cancelado) setDatos(json);      } catch (err) {        if (!cancelado) setError(err.message);      } finally {        if (!cancelado) setCargando(false);      }    }    cargar();    return () => { cancelado = true; };  }, [url]);  return { datos, cargando, error }; } // Usar el hook en cualquier componente function ListaProductos() {  const { datos, cargando, error } = useDatos('/api/productos');  if (cargando) return <p>Cargando productos...</p>;  if (error)    return <p>Error: {error}</p>;  return (    <ul>      {datos?.map(p => (        <li key={p.id}>{p.nombre} — {p.precio}€</li>      ))}    </ul>  ); } function PerfilUsuario({ id }) {  const { datos: usuario, cargando } = useDatos(`/api/usuarios/${id}`);  if (cargando) return <p>Cargando...</p>;  return <h2>{usuario?.nombre}</h2>; }

Resumen

  • useState añade estado a un componente. Devuelve el valor actual y una función para actualizarlo. Cuando el estado cambia, React re-renderiza el componente.
  • Nunca mutes el estado directamente: crea siempre un nuevo valor. Para objetos usa spread ({ ...obj, campo: valor }), para arrays usa map, filter o spread.
  • Usa la forma funcional del setter (setValor(prev => prev + 1)) cuando el nuevo valor depende del anterior, especialmente dentro de temporizadores o suscripciones.
  • useEffect sincroniza el componente con el mundo exterior: peticiones de red, eventos del DOM, temporizadores, suscripciones. Se ejecuta después del render.
  • El array de dependencias controla cuándo se ejecuta el efecto: sin array → cada render; [] → solo al montar; [dep] → cuando dep cambia.
  • Devuelve una función de limpieza para cancelar suscripciones, limpiar temporizadores y evitar actualizaciones de estado en componentes desmontados.
  • Para efectos asíncronos, define la función async dentro del efecto y llámala: no marques el callback de useEffect como async directamente.
  • Extrae la lógica repetida de useState + useEffect en hooks personalizados: hacen el código reutilizable y los componentes más limpios.
Etiquetas: React.js

¿Te ha gustado esta entrada?

Compártela con tus compañeros para que también sigan aprendiendo.

Comunidad y Comentarios

0 COMENTARIOS

No hay comentarios todavía. Sé el primero en compartir tu opinión.

Escribe tu opinión
Respondiendo a