Curso de React.js

Cómo evitar el bucle infinito en useEffect

Curso de React.js

Contenido del curso

Herramientas avanzadas: escalabilidad, organización y persistencia

Cómo evitar el bucle infinito en useEffect

Resumen

Manejar los estados de carga y error en React puede convertirse en una pesadilla si no proteges tu useEffect correctamente. Aquí verás cómo actualizar los estados de loading y error dentro de un custom hook con localStorage, envolver la lógica en un bloque try/catch, y evitar el temido bucle infinito que rompe tu aplicación.

Por qué mi estado nunca se actualiza desde localStorage

Al construir un custom hook para localStorage, es común crear los estados con React.useState y olvidar actualizarlos. Si defines item, loading y error con valores iniciales pero nunca llamas a setItem, setLoading o setError, tu aplicación se queda congelada en el estado inicial.

La solución dentro del efecto es directa: cuando termine la lectura de localStorage, llama a setLoading(false) para avisar que ya no estás cargando. Y si el valor parseado es distinto al initial value, llama a setItem(parsedItem) para reflejar los datos reales.

¿Por qué mi useState no refleja los datos de localStorage? Porque el estado se queda con el initial value si nunca invocas el setter. Debes llamar a setItem(parsedItem) dentro del efecto una vez que hayas leído y parseado la información.

Cómo capturar errores con try catch

localStorage puede fallar en cualquier momento y no controlas cuándo. Por eso envuelves la lógica de lectura en un bloque try y, si algo sale mal, entras al catch para actualizar el estado de error.

Dentro del catch tienes dos opciones:

  • Enviar el error tal cual con setError(error) si quieres saber el detalle.
  • Enviar un booleano setError(true) si solo necesitas indicar que algo falló.

Si vas a mostrar el mensaje a usuarios finales, quédate con un genérico tipo Hubo un error. Y no olvides que cuando cae en el catch también debes hacer setLoading(false), porque ya terminó el intento aunque haya fallado.

Cómo evitar el bucle infinito en useEffect

Una vez que agregas un setTimeout de 2000 milisegundos para simular una llamada asíncrona (como si consultaras una API REST), React deja de gritar con maximum update depth exceeded. Pero eso no significa que tu código esté sano.

El efecto sigue siendo un bucle infinito silencioso: el estado inicial dispara el efecto, el efecto actualiza el estado con setItem, setLoading o setError, ese cambio provoca un nuevo render, el hook se vuelve a llamar y el ciclo empieza otra vez. React no lo marca como error porque el setTimeout lo hace ver natural, pero está mal.

¿Cómo hago que useEffect se ejecute solo una vez? Debes pasarle un array de dependencias vacío como segundo argumento: useEffect(() => {}, []). Son tres caracteres, un espacio incluido, que se olvidan facilísimo.

Qué son los ciclos de vida y por qué importa useEffect

Antes de los hooks, React trabajaba con clases que extendían de React.Component y usaban métodos de ciclo de vida como componentDidMount o componentDidUpdate. Con las versiones más recientes, useEffect se convirtió en la alternativa funcional a esos métodos.

Es cómodo, pero también engañoso: olvidar el array de dependencias es un error tan común como difícil de detectar cuando no salta ninguna alerta roja en la consola.

Cómo depurar el famoso undefined en filter

Otro error típico que aparece en esta implementación es intentar llamar a .filter sobre algo que no es un array. La consola dice claramente que no puede leer propiedades de undefined, y eso apunta a que el estado derivado (searchedTodos, por ejemplo) está recibiendo basura.

Cuando pasa esto, sigue este flujo de depuración:

  1. Añade un console.log en el custom hook antes del return para ver qué valor tiene realmente item.
  2. Revisa la línea exacta que apunta el error en el archivo useLocalStorage.
  3. Verifica que los parámetros de la función estén bien declarados.

En este caso, el bug estaba en la firma del hook: por probar ejemplos, quedó escrito useLocalStorage(itemName, [initialValue]) con corchetes alrededor de initialValue. Eso convierte el parámetro en un patrón de array destructuring y rompe el valor recibido. La corrección es dejarlo como un parámetro normal: useLocalStorage(itemName, initialValue).

Cuándo borrar el localStorage en desarrollo

Si sospechas que el problema viene de datos corruptos guardados en el navegador, puedes borrar todo el localStorage desde la consola. En producción con usuarios reales esto sería catastrófico, pero en desarrollo es una herramienta válida para volver a un estado limpio y ejecutar los default todos que siempre funcionan.

Eso sí, si el error persiste después de limpiar, la falla no está en los datos sino en tu lógica, como pasó con el parámetro entre corchetes.

Cómo simular una carga asíncrona con setTimeout

Para imitar el comportamiento de una petición real y ver los estados de carga en acción, envuelve toda la lógica del try/catch dentro de un setTimeout con un delay de 2000 milisegundos. No uses setInterval, porque ese ejecutaría la función repetidamente.

La función que pasas como primer argumento contiene tu bloque try/catch completo, y el segundo argumento define cuántos milisegundos esperar antes de ejecutarla. Con esto, al recargar la aplicación verás por dos segundos el mensaje Estamos cargando antes de que aparezcan los TODOs, exactamente como pasaría con una API real.

¿Ya encontraste los tres caracteres que resuelven el bucle infinito en tu propio código? Cuéntame en los comentarios cómo llegaste a la solución o qué caminos exploraste durante la búsqueda.