Cómo solucionar el bucle infinito en useEffect con objetos y arrays
Explicación técnica
El problema ocurre porque useEffect compara los valores de las dependencias usando comparación de referencia (===), no por contenido (shallow comparison). Cuando usas useState({}), cada llamada a setObj({}) crea un nuevo objeto en memoria, aunque tenga el mismo contenido. Por lo tanto:
-
{} === {}→false -
[1,2] === [1,2]→false
En tu caso:
const [ingredients, setIngredients] = useState({});
useEffect(() => {
setIngredients({}); // ← ¡Crea un nuevo objeto!
}, [ingredients]); // ← Siempre es una referencia distinta → loop infinito
React detecta que la referencia cambia y vuelve a ejecutar el efecto, que a su vez vuelve a cambiar la referencia, creando un bucle infinito.
Solución definitiva (3 enfoques)
✅ Opción 1: Evitar la actualización innecesaria (recomendada)
Si no necesitas actualizar el estado (como en tu ejemplo donde pones {} en {}), no llames al setter:
useEffect(() => {
// Si el estado ya es {}, no necesitas hacer nada
// Solo ejecuta lógica si hay algo que hacer
}, []); // Dependencia vacía: solo corre una vez al montar
✅ Opción 2: Comparación por contenido con useDeepCompareEffect
Crea un hook personalizado que compare por profundidad (deep comparison):
import { useEffect, useRef } from 'react';
function useDeepCompareEffect(callback, dependencies) {
const previousDeps = useRef(dependencies);
const changedDeps = dependencies.some((dep, i) =>
!Object.is(dep, previousDeps.current[i])
);
if (changedDeps) {
previousDeps.current = dependencies;
useEffect(callback, dependencies);
}
}
// Uso:
useDeepCompareEffect(() => {
setIngredients({});
}, [ingredients]);
⚠️ Nota: Usa librerías como
use-deep-compare-effecten producción para evitar bugs.
✅ Opción 3: Normalizar el estado y usar valores primitivos
Convierte objetos/array en valores primitivos para comparación:
const [ingredients, setIngredients] = useState([]);
const [ingredientsCount, setIngredientsCount] = useState(0);
useEffect(() => {
// Solo actualiza si el conteo cambia
if (ingredients.length !== ingredientsCount) {
setIngredientsCount(ingredients.length);
// Tu lógica aquí
}
}, [ingredientsCount, ingredients.length]);
Bloque de código corregido (ejemplo práctico)
Antes (bucle infinito):
const [ingredients, setIngredients] = useState({});
useEffect(() => {
setIngredients({}); // ← ¡Causa loop!
}, [ingredients]);
Después (solución limpia):
const [ingredients, setIngredients] = useState({});
useEffect(() => {
// Solo ejecutar si hay lógica real que necesite ejecutarse
// Si solo quieres inicializar, usa una bandera o dependencia vacía
}, []); // Dependencia vacía: ejecuta una vez al montar
O si necesitas resetear bajo condiciones específicas:
const [ingredients, setIngredients] = useState({});
const [resetTrigger, setResetTrigger] = useState(false);
useEffect(() => {
if (resetTrigger) {
setIngredients({});
setResetTrigger(false); // Resetear la bandera
}
}, [resetTrigger]);
Pro-tip: Regla de oro para hooks con objetos/array
Nunca uses objetos/array como dependencias directas si vas a reasignarlos dentro del
useEffect.
En su lugar:
- Usa valores primitivos (IDs, longitudes, timestamps)
- Implementa comparación profunda con librerías probadas
- Reconsidera si realmente necesitas actualizar el estado (¿ya tiene el valor que quieres asignar?)
Bonus: Si usas immer o useImmer, puedes evitar este problema con inmutabilidad controlada.
Top comments (0)