React quiere que tus componentes sean predecibles. Dale el mismo estado y las mismas props, y debería pintar la misma interfaz de usuario cada vez. Pero la mayoría de las aplicaciones reales no pueden sobrevivir dentro de esa burbuja. Necesitan interactuar con el exterior. Un tablero necesita datos frescos de un servidor. Un widget de chat necesita escuchar mensajes. Un temporizador necesita avanzar. Estas operaciones son efectos secundarios (side effects) y se encuentran fuera del ciclo de renderizado de React. El hook useEffect es donde colocas ese trabajo desordenado e impredecible para que tu componente en sí se mantenga íntegro.
Efectos secundarios: qué debe ir dentro de useEffect
Un efecto secundario es cualquier cosa que afecte al mundo más allá de devolver JSX. La fase de renderizado de React debe ser pura. Cuando comienzas a obtener datos, escribir en variables globales o adjuntar escuchadores (listeners) al DOM, has salido del territorio de la pureza.
Ejemplos comunes incluyen:
- Obtener datos de una API
- Configurar temporizadores o intervalos
- Añadir escuchadores de eventos a
windowodocument - Actualizar el título de la pestaña del navegador
- Conectarse a WebSockets
Estas tareas comparten un rasgo: no pertenecen a la sentencia return de tu componente ni a la lógica principal de renderizado. Intentar llamar a una API del navegador como setInterval directamente dentro del cuerpo del renderizado provocará que se ejecute en cada renderizado, creando temporizadores duplicados y comportamientos confusos. useEffect existe precisamente para aislar este trabajo y ejecutarlo en el momento correcto.
Cómo el array de dependencias controla el tiempo de ejecución
El segundo argumento de useEffect es el array de dependencias, y es la mayor fuente de confusión para los desarrolladores que vienen de los componentes de clase. Piénsalo como un conjunto de variables que React observa para decidir si omitir o ejecutar tu efecto después del renderizado actual.
Hay tres patrones que usarás repetidamente.
Sin ningún array de dependencias. Si omites el array por completo, React asume que quieres que el efecto se ejecute después de cada renderizado, incluido el primero. Esto rara vez es lo que necesitas. Si tu efecto realiza una solicitud de red o una operación pesada en el DOM, ejecutarlo con cada pulsación de tecla o ajuste de estado arruinará el rendimiento. Usa este patrón solo cuando realmente necesites volver a ejecutar algo porque cualquier prop o estado podría haber cambiado y no puedes especificar cuáles.
Un array vacío []. Esto le indica a React que ejecute el efecto una sola vez, inmediatamente después de que el componente se monte y el DOM esté listo. Este es el lugar adecuado para las peticiones de datos iniciales. Por ejemplo, si tu componente carga datos del perfil de usuario, querrás que esa solicitud se dispare exactamente una vez cuando aparezca la página del perfil, no cada vez que el usuario interactúe con un formulario más abajo en la página.
Un array con variables específicas [count]. Esta es la herramienta de precisión. React compara los valores actuales de estas dependencias con sus valores durante el último renderizado. Si alguno de ellos cambia, el efecto se ejecuta. Si nada en la lista cambia, React omite el efecto por completo.
Si estás sincronizando el título de la pestaña del navegador con una variable de estado, pondrías esa variable en el array de dependencias. React actualizará el título solo cuando ese valor cambie. Si lo dejas fuera, el título se quedará desactualizado. Si incluyes variables de estado no relacionadas, desperdiciarás ciclos actualizando el título por cambios que no importan.
La limpieza no es opcional
Algunos efectos dejan huellas. Un temporizador sigue contando. Un escuchador de eventos sigue disparándose. Un WebSocket permanece abierto. Cuando tu componente se desmonta, o incluso cuando un efecto se vuelve a ejecutar porque sus dependencias cambiaron, React no limpia automáticamente el residuo del efecto anterior. Ese es tu trabajo.
Creas una función de limpieza devolviendo una función desde el interior de useEffect. React llama a esta limpieza antes de aplicar el siguiente efecto, y una vez más cuando el componente sale de la pantalla.
Deberías usar la limpieza para:
- Limpiar intervalos o tiempos de espera con
clearIntervaloclearTimeout - Eliminar escuchadores de eventos añadidos a
window,documento nodos externos - Cancelar la suscripción a flujos de datos o servicios
Si descuidas esto, tendrás fugas de memoria (memory leaks). Un componente se monta, adjunta un escuchador de desplazamiento (scroll listener), se desmonta y el escuchador permanece. El navegador mantiene la función de callback y los nodos del DOM a los que hace referencia. Con el tiempo, especialmente en aplicaciones de una sola página (SPA) con mucha navegación, estos "fantasmas" se acumulan y ralentizan la pestaña. La solución suele ser solo unas pocas líneas: devuelve una función que elimine lo que añadiste.
Errores comunes que llegan a producción
Incluso los desarrolladores experimentados recurren a useEffect cuando existe una opción más sencilla. Aquí hay tres patrones que deberían ser una señal de alerta durante una revisión de código.
Bucles infinitos. Nunca actualices una variable de estado dentro de useEffect si esa misma variable se encuentra en tu array de dependencias, a menos que tengas una condición de control que rompa el ciclo. Si lees count, lo incrementas y enumeras count como una dependencia, React detecta el cambio, vuelve a renderizar, ejecuta el efecto de nuevo, lo incrementa otra vez y bloquea el navegador.
Efectos innecesarios. No utilices useEffect para calcular un valor a partir de props o del estado existentes. Si puedes derivarlo directamente durante el renderizado, simplemente hazlo. Los valores derivados deben pertenecer al cuerpo del componente o a un cálculo memorizado con useMemo. Moverlos a un efecto divide tu lógica entre las fases de renderizado y de efecto sin ningún beneficio y hace que el código sea más difícil de seguir.
La herramienta equivocada para las acciones del usuario. `use
