En esta página

useEffect y Ciclo de Vida

14 min lectura TextoCap. 2 — Estado y efectos

El propósito de useEffect

useEffect sirve para sincronizar un componente con un sistema externo: una API REST, el DOM del navegador, un WebSocket, una suscripción de eventos, localStorage, etc.

No es un reemplazo para la lógica de render ni debe usarse para cálculos derivados del estado. Su propósito específico es efectos secundarios que ocurren como consecuencia de un render.

// Conceptualmente, useEffect funciona así:
useEffect(() => {
  // Código que se ejecuta DESPUÉS del render
  // Puede retornar una función de limpieza

  return () => {
    // Se ejecuta ANTES del próximo efecto o al desmontar
  };
}, [/* dependencias */]);

El array de dependencias

El segundo argumento de useEffect controla cuándo se ejecuta el efecto:

// 1. Sin array: se ejecuta después de CADA render
useEffect(() => {
  console.log('Después de cada render');
});

// 2. Array vacío: solo al montar (componentDidMount)
useEffect(() => {
  console.log('Solo al montar');
  return () => console.log('Solo al desmontar');
}, []);

// 3. Con dependencias: cuando alguna dependencia cambia
useEffect(() => {
  console.log('userId cambió:', userId);
}, [userId]);

// 4. Múltiples dependencias
useEffect(() => {
  console.log('página o filtro cambió');
}, [pagina, filtro]);

Regla fundamental: todas las variables reactivas (props, estado) que se usan dentro del efecto deben estar en el array de dependencias. El linter de React te advertirá si omites alguna.

Función de limpieza

La función de limpieza (cleanup) previene memory leaks y comportamiento inesperado:

// Ejemplo: timer con limpieza
function TemporizadorRegresivo({ segundos }: { segundos: number }): React.JSX.Element {
  const [restante, setRestante] = useState(segundos);

  useEffect(() => {
    if (restante <= 0) return;

    const id = setInterval(() => {
      setRestante((prev) => {
        if (prev <= 1) {
          clearInterval(id);
          return 0;
        }
        return prev - 1;
      });
    }, 1000);

    // Sin esto, el intervalo continuaría después de desmontar
    return () => clearInterval(id);
  }, [restante]);

  return <p>{restante > 0 ? `${restante}s restantes` : '¡Tiempo!'}</p>;
}

Strict Mode y la doble invocación

En React 18+ con Strict Mode (activado por defecto en desarrollo), cada efecto se ejecuta dos veces: monta → desmonta → vuelve a montar. Esto es intencional y sirve para detectar efectos sin limpieza adecuada.

// ❌ Problema: sin limpieza, el doble montaje crea dos suscripciones
useEffect(() => {
  const sub = servicio.suscribir(callback);
  // Sin retorno de limpieza
}, []);

// ✅ Correcto: la limpieza cancela la suscripción antes del remontaje
useEffect(() => {
  const sub = servicio.suscribir(callback);
  return () => sub.cancelar();
}, []);

Fetching de datos — patrones correctos

El fetching en useEffect es un caso de uso común pero tiene sus sutilezas:

El problema de las condiciones de carrera

Si el usuario cambia el postId rápidamente, pueden llegar respuestas fuera de orden:

useEffect(() => {
  // ❌ Sin control: la respuesta del post 1 puede llegar después del post 2
  fetch(`/api/posts/${postId}`).then((res) => res.json()).then(setPost);
}, [postId]);

// ✅ Con AbortController: la petición anterior se cancela
useEffect(() => {
  const ctrl = new AbortController();

  fetch(`/api/posts/${postId}`, { signal: ctrl.signal })
    .then((res) => res.json() as Promise<Post>)
    .then(setPost)
    .catch((err: unknown) => {
      if (err instanceof Error && err.name !== 'AbortError') {
        setError(err.message);
      }
    });

  return () => ctrl.abort();
}, [postId]);

Estado de carga correcto

interface EstadoFetch<T> {
  datos: T | null;
  cargando: boolean;
  error: string | null;
}

function usarDatos<T>(url: string): EstadoFetch<T> {
  const [estado, setEstado] = useState<EstadoFetch<T>>({
    datos: null,
    cargando: true,
    error: null,
  });

  useEffect(() => {
    const ctrl = new AbortController();
    setEstado({ datos: null, cargando: true, error: null });

    fetch(url, { signal: ctrl.signal })
      .then((res) => {
        if (!res.ok) throw new Error(`Error ${res.status}`);
        return res.json() as Promise<T>;
      })
      .then((datos) => setEstado({ datos, cargando: false, error: null }))
      .catch((err: unknown) => {
        if (err instanceof Error && err.name === 'AbortError') return;
        setEstado({
          datos: null,
          cargando: false,
          error: err instanceof Error ? err.message : 'Error desconocido',
        });
      });

    return () => ctrl.abort();
  }, [url]);

  return estado;
}

Errores comunes y cómo evitarlos

1. Bucle infinito

// ❌ Error: `datos` cambia en cada render → efecto se ejecuta infinitamente
useEffect(() => {
  setDatos(transformar(datos));
}, [datos]);

// ✅ Calcular derivados durante el render, no en efectos
const datosTransformados = transformar(datos);

2. Dependencias faltantes

// ❌ Error: `usuario` no está en dependencias, pero se usa dentro
useEffect(() => {
  fetch(`/api/perfil/${usuario.id}`).then(setPerfil);
}, []); // El linter te advertirá

// ✅ Incluir todas las dependencias
useEffect(() => {
  fetch(`/api/perfil/${usuario.id}`).then(setPerfil);
}, [usuario.id]);

3. Efectos asíncronos mal escritos

// ❌ No puedes marcar el callback de useEffect como async directamente
useEffect(async () => {
  const data = await fetch('/api/datos');
  // El valor de retorno async no es una función de limpieza
}, []);

// ✅ Define la función async dentro del efecto
useEffect(() => {
  const cargar = async () => {
    const res = await fetch('/api/datos');
    const data = await res.json() as Datos;
    setDatos(data);
  };

  void cargar();
}, []);

Cuándo NO usar useEffect

Con React moderno, muchos usos tradicionales de useEffect ya no son necesarios:

  • Calcular estado derivado: usa variables calculadas durante el render o useMemo
  • Inicializar datos del servidor: usa async loaders en React Router o Next.js
  • Sincronizar dos estados: redefine el estado para evitar la duplicación
  • Responder a eventos: usa event handlers directamente

useEffect debe ser tu última opción cuando nada más funciona. Si sientes que lo usas mucho, probablemente haya una forma más simple de resolver el problema.

useEffect no es el lugar para lógica síncrona derivada
Si usas useEffect para calcular un valor a partir de props o estado y luego guardarlo en otro estado, estás complicando el flujo. En su lugar, calcula el valor directamente durante el render o usa useMemo. useEffect es para sincronizar con sistemas externos (APIs, DOM, timers, suscripciones).
Strict Mode ejecuta los efectos dos veces en desarrollo
React 18+ en Strict Mode monta, desmonta y vuelve a montar cada componente en desarrollo para detectar efectos sin limpieza. Si tu efecto parece ejecutarse dos veces, es normal en desarrollo. La solución correcta es implementar siempre la función de limpieza.
El eslint-plugin-react-hooks te ayuda con las dependencias
Instala eslint-plugin-react-hooks para obtener advertencias automáticas cuando omites dependencias en el array. Seguir sus recomendaciones evita el bug más común de useEffect: los closures estales donde el efecto usa valores desactualizados.