DEV Community

Erick Eduardo Ramos
Erick Eduardo Ramos

Posted on

Cómo solucionar el bucle infinito en `useEffect` con objetos y arrays

Cómo solucionar el bucle infinito en useEffect con objetos y arrays

Explicación técnica (por qué ocurre)

El problema ocurre porque React compara los dependencies del useEffect usando comparación === (igualdad estricta), no una comparación profunda del contenido.

Cuando haces:

const [obj, setObj] = useState({});
useEffect(() => {
  setObj({});
}, [obj]);
Enter fullscreen mode Exit fullscreen mode

Cada vez que setObj({}) se ejecuta, crea un nuevo objeto en memoria. Aunque el contenido parece idéntico, {} !== {} siempre es true. Por lo tanto:

  1. useEffect detecta que obj ha cambiado (nueva referencia)
  2. Ejecuta la callback → setObj({}) crea otro nuevo objeto
  3. React vuelve a detectar cambio de referencia → bucle infinito

Solución definitiva (3 enfoques prácticos)

✅ Opción 1: Evita el ciclo usando dependencias vacías (si solo necesitas ejecutar al montar)

useEffect(() => {
  setIngredients({});
}, []); // Solo se ejecuta una vez al montar el componente
Enter fullscreen mode Exit fullscreen mode

✅ Opción 2: Usa comparación profunda con JSON.stringify (para efectos que dependen del contenido)

useEffect(() => {
  // Solo ejecutar si el contenido de ingredients ha cambiado
  if (JSON.stringify(ingredients) !== JSON.stringify(prevIngredientsRef.current)) {
    // Tu lógica aquí
  }
}, [ingredients]);
Enter fullscreen mode Exit fullscreen mode

⚠️ Nota: JSON.stringify tiene limitaciones (no preserva funciones, undefined, Symbol, orden de claves no garantizado). Úsalo solo para objetos simples.

✅ Opción 3: Usa useReducer + estado inmutable (recomendado para lógica compleja)

const initialState = {};
const reducer = (state, action) => {
  switch (action.type) {
    case 'reset':
      return {}; // Devuelve nuevo objeto, pero controlado
    default:
      return state;
  }
};

const [ingredients, dispatch] = useReducer(reducer, initialState);

useEffect(() => {
  dispatch({ type: 'reset' });
}, []); // O con lógica específica según tu caso
Enter fullscreen mode Exit fullscreen mode

Bloque de código corregido (ejemplo práctico)

Antes (bucle infinito):

const [ingredients, setIngredients] = useState({});

useEffect(() => {
  setIngredients({}); // ❌ Crea nuevo objeto → bucle infinito
}, [ingredients]); // ❌ Referencia cambia siempre
Enter fullscreen mode Exit fullscreen mode

Después (solución limpia):

const [ingredients, setIngredients] = useState({});

useEffect(() => {
  // ✅ Solo resetear si no está vacío (evita ciclos innecesarios)
  if (Object.keys(ingredients).length > 0) {
    setIngredients({});
  }
}, [ingredients]);
Enter fullscreen mode Exit fullscreen mode

Pro-tip: Patrón para evitar este error en producción

  1. Nunca uses objetos/arrays literales como dependencias directas ([{}], [[]])
  2. Usa useRef para valores que no deben desencadenar re-renders:
   const ingredientsRef = useRef({});
   useEffect(() => {
     ingredientsRef.current = {}; // Modifica sin causar re-renders
   }, []);
Enter fullscreen mode Exit fullscreen mode
  1. Para objetos complejos, usa librerías como immer o useDeepCompareEffect (de use-deep-compare-effect):
   import useDeepCompareEffect from 'use-deep-compare-effect';

   useDeepCompareEffect(() => {
     setIngredients({});
   }, [ingredients]); // ✅ Compara contenido, no referencias
Enter fullscreen mode Exit fullscreen mode

🔥 Regla de oro: Si tu useEffect modifica el mismo estado que usa como dependencia, siempre debes verificar si el cambio es necesario antes de llamar al setter.

Top comments (0)