12k
All articles

Por qué tu useEffect se ejecuta dos veces

Descubre por qué React StrictMode ejecuta useEffect dos veces en desarrollo y cómo corregir solicitudes, escuchas y otros efectos con una limpieza adecuada.

OpenReplay Team
OpenReplay Team
Por qué tu useEffect se ejecuta dos veces

En desarrollo, el StrictMode de React ejecuta la configuración (setup) de cada efecto, luego su limpieza (cleanup) y después la configuración otra vez. Por eso, cualquier efecto que haga un fetch, se suscriba a algo o escriba en la consola ocurrirá visiblemente dos veces.

Escribes un solo fetch en un efecto, abres la pestaña Network y aparecen dos peticiones idénticas y dos logs en la consola. Parece un bug en tu código o en React.

No es ninguna de las dos cosas. La doble ejecución es una prueba deliberada de tu lógica de limpieza. Este artículo explica qué hace StrictMode, cómo corregir los tres efectos más comunes que no superan la prueba, qué “soluciones” solo ocultan el problema y cómo saber cuándo has terminado. Si necesitas repasar el hook en sí, empieza por esta guía del hook useEffect de React.

Puntos clave

  • StrictMode ejecuta un ciclo adicional de configuración y limpieza para cada efecto, solo en desarrollo, tanto en React 18 como en React 19.
  • StrictMode es opcional, pero la plantilla de React de Vite envuelve la aplicación en <StrictMode>, y el App Router de Next.js lo activa por defecto.
  • Una función de limpieza correcta deshace exactamente lo que hizo la configuración: aborta la petición, elimina el listener, limpia el temporizador o cierra la conexión.
  • Ver dos peticiones en la pestaña Network durante el desarrollo es lo esperado, incluso después de aplicar una corrección adecuada. Lo importante es que solo la respuesta más reciente pueda actualizar el estado.
  • Eliminar StrictMode o añadir una guarda de useRef del tipo “ya se ejecutó” oculta la falta de limpieza sin corregirla.

El síntoma: un fetch que se dispara dos veces

Un efecto de obtención de datos sin limpieza es la forma más habitual en que la gente descubre este comportamiento. Este componente funciona bien en producción y se dispara dos veces en desarrollo:

import { useState, useEffect } from 'react';

function ProductDetails({ id }) {
  const [product, setProduct] = useState(null);

  useEffect(() => {
    console.log('setup');
    fetch(`/api/products/${id}`)
      .then((res) => res.json())
      .then((data) => setProduct(data));
  }, [id]);

  return <h2>{product?.name}</h2>;
}

Obtienes dos logs setup y dos peticiones. Nada le indica a React cómo cancelar la primera petición, así que ambas se ejecutan hasta completarse.

¿Por qué useEffect se ejecuta dos veces en desarrollo?

La referencia de StrictMode de React explica que, con StrictMode activado, React añade una pasada extra de configuración y limpieza a cada efecto durante el desarrollo. Tu efecto ejecuta la configuración, luego la limpieza y luego la configuración de nuevo, como si el componente se montara, se desmontara y se volviera a montar de inmediato.

La doble ejecución solo ocurre en builds de desarrollo. En producción, un efecto se ejecuta una vez cuando el componente se monta, y de nuevo solo cuando cambian sus dependencias o el componente se vuelve a montar.

StrictMode es opcional: solo se aplica a los componentes envueltos en <StrictMode>. Sin embargo, muchos proyectos vienen envueltos por defecto. El main.jsx de la plantilla de React de Vite renderiza <App /> dentro de <StrictMode>. El App Router de Next.js activa StrictMode por defecto desde Next.js 13.5.1. Las aplicaciones con Pages Router tienen que activarlo explícitamente con reactStrictMode: true.

El ciclo comprueba si tu limpieza funciona de verdad. Si un efecto se comporta mal cuando React ejecuta configuración, limpieza y configuración, también se comportará mal cuando un usuario navegue a otra página y vuelva. Y también fallará cuando las ediciones de código lo vuelvan a ejecutar durante el desarrollo: la documentación de Fast Refresh de Next.js indica que los efectos deben tolerar reejecuciones ocasionales y señala que StrictMode lo impone. Para ver en detalle cuándo se ejecuta exactamente la limpieza respecto al pintado, consulta useEffect vs useLayoutEffect.

¿Cómo se corrige un useEffect que se ejecuta dos veces?

Para corregir un useEffect que se ejecuta dos veces, dale una función de limpieza que revierta lo que inició la configuración. Cada fallo común tiene una solución específica:

Lo que ves en desarrolloLo que realmente fallaSolución
Dos fetches, posibles datos obsoletosNada cancela la petición en cursoAbortController o un flag ignore en la limpieza
Un listener o suscripción se dispara dos vecesNunca se eliminaEliminarlo en la limpieza
Un contador termina en 2La lógica no es un efecto secundarioMoverla a un manejador de eventos

Un fetch que se dispara dos veces

Un fetch que se dispara dos veces en useEffect tiene dos soluciones. La primera es abortar la petición en la limpieza. Cuando se aborta un fetch, este se rechaza con una DOMException de tipo AbortError, que puedes ignorar sin problema:

useEffect(() => {
  const controller = new AbortController();
  fetch(`/api/products/${id}`, { signal: controller.signal })
    .then((res) => res.json())
    .then((data) => setProduct(data))
    .catch((err) => {
      if (err.name !== 'AbortError') throw err;
    });
  return () => controller.abort();
}, [id]);

La otra opción es el flag ignore, que es el patrón que usa la guía de React sobre sincronización con efectos para la obtención de datos:

useEffect(() => {
  let ignore = false;
  fetch(`/api/products/${id}`)
    .then((res) => res.json())
    .then((data) => {
      if (!ignore) setProduct(data);
    });
  return () => {
    ignore = true;
  };
}, [id]);

Seguirás viendo dos peticiones en desarrollo con cualquiera de las dos soluciones. Con AbortController, la primera petición se aborta. Con ignore, ambas peticiones se completan, pero solo la segunda puede actualizar el estado. En producción se envía una sola petición. Esta misma limpieza también evita que una respuesta lenta sobrescriba otra más reciente cuando cambia id.

Un listener con fugas

Un event listener añadido en useEffect provoca una fuga a menos que la limpieza lo elimine. Declara el manejador dentro del efecto para que la limpieza elimine la misma referencia de función que añadió la configuración:

useEffect(() => {
  function handleResize() {
    setWidth(window.innerWidth);
  }
  window.addEventListener('resize', handleResize);
  return () => window.removeEventListener('resize', handleResize);
}, []);

Un contador que se incrementa dos veces

// Before: ends at 2 in development
useEffect(() => {
  setCount((c) => c + 1);
}, []);

La configuración se ejecuta dos veces en desarrollo, así que el incremento también. Ninguna limpieza puede “desincrementar” el contador, y eso te indica que el efecto no debería existir. Coloca el incremento donde ocurre el evento que lo desencadena:

<button onClick={() => setCount((c) => c + 1)}>Add</button>

¿Deberías eliminar StrictMode o añadir una guarda con useRef?

No. Ambos enfoques hacen desaparecer la doble ejecución y dejan intacto el bug subyacente.

Desactivar StrictMode, ya sea eliminando el wrapper o configurando reactStrictMode: false en tu configuración de Next.js, elimina la prueba. Los efectos siguen sin tener limpieza.

La guarda con useRef es la opción más tentadora:

// Avoid: hides missing cleanup
const hasRun = useRef(false);

useEffect(() => {
  if (hasRun.current) return;
  hasRun.current = true;
  subscribe();
}, []);

La guarda omite la segunda configuración en desarrollo, así que la consola parece limpia. Pero un flag de useRef que omite la segunda ejecución no corrige una limpieza ausente: oculta el problema en desarrollo y deja la fuga en cada remontaje real. Una ref pertenece a una única instancia del componente. Cuando una ruta se desmonta y se vuelve a montar en producción, la nueva instancia obtiene una nueva ref, por lo que el efecto se ejecuta de nuevo. Como sigue sin haber limpieza, la suscripción anterior nunca se elimina.

En un session replay de una aplicación sin limpieza en sus efectos, este bug puede manifestarse como peticiones duplicadas o datos obsoletos tras navegar hacia atrás y hacia adelante. Es el mismo bug que StrictMode saca a la luz en desarrollo.

Cuándo no necesitas un efecto

Muchos efectos que se disparan dos veces nunca deberían haber sido efectos. Los efectos sirven para sincronizarse con algo externo a React porque el componente está en pantalla. Quizás no necesites un efecto aborda los dos grandes grupos de efectos innecesarios.

El trabajo desencadenado por eventos pertenece a los manejadores de eventos. Enviar un formulario, mostrar un toast tras pulsar “Añadir al carrito” o incrementar un contador ocurre porque el usuario hizo algo, no porque un componente se renderizó.

Los valores que puedes calcular a partir de props o del estado pertenecen al renderizado:

// Before: extra state, extra render, effect runs twice
const [fullName, setFullName] = useState('');
useEffect(() => {
  setFullName(first + ' ' + last);
}, [first, last]);

// After: computed during render
const fullName = first + ' ' + last;

Una comprobación rápida: ¿tu efecto es correcto?

Para comprobar si un useEffect es correcto, añade logs tanto en la configuración como en la limpieza:

useEffect(() => {
  console.log('setup');
  return () => console.log('cleanup');
}, []);

En desarrollo, la consola debería mostrar:

setup
cleanup
setup

Un build de producción registra setup una sola vez. Si tus logs muestran setup, cleanup, setup y la interfaz termina en el estado correcto, el efecto funciona como debe y no hay nada que corregir.

Conclusión

Un useEffect que se ejecuta dos veces en desarrollo es StrictMode comprobando si tu limpieza funciona. No lo silencies. Revisa cada efecto que se dispare dos veces y haz que supere la prueba: aborta o ignora las peticiones obsoletas, elimina lo que añades y saca por completo de los efectos la lógica desencadenada por eventos y los valores derivados. Una vez que tus efectos limpien correctamente, añade error boundaries de React para que un componente que lance un error durante el renderizado no tumbe toda la página.

Preguntas frecuentes

¿Por qué mi useEffect se ejecuta dos veces en producción, donde StrictMode está desactivado?

En producción, un efecto se vuelve a ejecutar cuando una de sus dependencias cambia de valor o cuando el componente se vuelve a montar. Si alguna dependencia difiere de la del último renderizado, el efecto se ejecuta de nuevo, por lo que los objetos y funciones creados durante el renderizado cuentan como valores nuevos cada vez. Cambiar la prop key, o un renderizado condicional que elimina y vuelve a añadir el componente, también lo remonta y ejecuta de nuevo la configuración.

¿Debería evitar que un evento de analítica se dispare dos veces en desarrollo?

No. La documentación de React recomienda dejar la llamada de analítica de visita de página en su efecto. Los usuarios no pueden saber si se ejecutó una o dos veces, y un build de producción envía cada visita una sola vez. Además, tu máquina de desarrollo no debería enviar eventos a las métricas de producción. Si necesitas depurar los eventos, haz pruebas en un build de staging que se ejecute en modo producción, o desactiva StrictMode durante un rato.

¿useLayoutEffect también se ejecuta dos veces con StrictMode?

Sí. La referencia de React para useLayoutEffect describe el mismo comportamiento en desarrollo que useEffect: con StrictMode activado, React realiza primero una pasada de configuración y limpieza, y después la configuración real. Los layout effects necesitan la misma limpieza correspondiente, como desconectar un observer o destruir la instancia de un widget de terceros. Cambiar a useLayoutEffect modifica cuándo se ejecuta el efecto respecto al pintado, no cuántas veces se ejecuta en desarrollo.

¿Puedo desactivar StrictMode para un componente y mantenerlo en el resto de la aplicación?

No. Una vez que un árbol está envuelto en StrictMode, todos los componentes que contiene reciben las comprobaciones, y ningún componente individual puede excluirse. Lo que sí puedes hacer es mover el wrapper de StrictMode más abajo en el árbol para que cubra solo algunas partes de la aplicación. Si StrictMode no envuelve la raíz, React omite la ejecución extra de efectos en el primer montaje. Esa ejecución haría que los efectos de los hijos se dispararan dos veces mientras que los de sus padres se dispararían una sola, algo que no puede ocurrir en producción.

DevTools for the frontend

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.