Un componente de chat reconecta su WebSocket cada vez que escribes una letra en un buscador que no tiene nada que ver con el chat. Otro componente se re-renderiza en bucle hasta que la pestaña se congela. Un tercero registra su useEffect dos veces al montarse, pero solo en desarrollo. Ninguno de estos es un bug de React. Los dos primeros son el array de dependencias haciendo exactamente lo que le pediste, y el tercero es Strict Mode comprobando que tu efecto sabe limpiar después de sí mismo.
Cuándo usar useEffect en un componente React
useEffect sincroniza un componente con algo que vive fuera de React: una conexión a un servidor, una API del navegador, un temporizador, el DOM directamente, un widget de terceros. React renderiza tu JSX, lo aplica al DOM, y solo entonces ejecuta tus efectos. Cuando el render no lo causó una interacción como un clic, React suele dejar que el navegador pinte primero la pantalla nueva.
Así que un efecto es el sitio equivocado para calcular un valor destinado al render. Si fullName se puede derivar de firstName y lastName, escríbelo durante el render:
| |
No un efecto que llame a setFullName después del hecho. La versión con efecto renderiza una vez con el valor desactualizado, programa una actualización de estado, y vuelve a renderizar. Ese paso extra no aporta nada. Recurre a useEffect cuando el componente necesita hacer algo porque se mostró, no para calcular algo que podría calcular directamente.
Cómo funciona el array de dependencias de useEffect
El segundo argumento de useEffect viene en tres formas, y cada una le dice a React algo distinto sobre cuándo volver a ejecutar la función.
| |
Para un array con elementos, React compara cada entrada con su valor en el render anterior usando Object.is. Es la misma comprobación que ===, salvo que NaN es igual a sí mismo y 0 y -0 son distintos. Si cada dependencia es idéntica a la última vez, React se salta el efecto. Si una difiere, lo ejecuta otra vez. No hay coincidencia parcial, ni forma de decirle a React “vuelve a ejecutar solo cuando cambie esta dependencia en concreto”. Esa lógica tiene que vivir dentro del efecto.
También significa que no puedes dejar un valor fuera del array para evitar que el efecto se vuelva a ejecutar. El array debe listar cada valor reactivo que el cuerpo de la función lee: estado, props, y cualquier cosa derivada de ellos. Quita un valor del array pero sigue leyéndolo, y el efecto corre con una copia desactualizada. La regla react-hooks/exhaustive-deps de eslint-plugin-react-hooks existe justamente para detectar ese desajuste. Trátala como un error de build, no la silencies con un comentario de desactivación.
Qué hace la función de cleanup de useEffect
Un efecto puede devolver una función. React trata ese valor de retorno como el cleanup de lo que el efecto haya iniciado:
| |
React llama al cleanup en dos situaciones: justo antes de que el efecto se vuelva a ejecutar porque una dependencia cambió, y cuando el componente se desmonta. Siempre limpia el efecto anterior antes de iniciar el siguiente. Conecta, desconecta, conecta, desconecta. Nunca dos conexiones abiertas a la vez.
Cualquier cosa que un efecto inicie y que necesite una parada explícita va detrás de una función de cleanup: subscribe necesita unsubscribe, addEventListener necesita removeEventListener, setInterval necesita clearInterval. El mismo patrón resuelve un bug más sutil al traer datos, donde una respuesta vieja llega después de una más nueva:
| |
Si personId cambia antes de que la primera petición se resuelva, el cleanup pone ignore en true para esa petición, así que su callback .then() no hace nada en lugar de sobrescribir un estado más reciente con datos más viejos.
Por qué useEffect se ejecuta dos veces en desarrollo
En desarrollo, bajo StrictMode, React ejecuta el setup, luego el cleanup, y luego el setup otra vez, inmediatamente después del primer montaje. En producción el setup se ejecuta una sola vez. React lo hace a propósito: tu componente se va a desmontar y volver a montar en uso real (navegar fuera de una ruta y volver, por ejemplo), y Strict Mode fuerza que eso pase primero en tu máquina, donde un cleanup que falta se ve como un bug visible.
El error es parchear el síntoma:
| |
El guard bloquea la segunda llamada a connect(), pero sigue sin haber cleanup, así que la conexión queda abierta después de que el componente se desmonta. Si volver a montar rompe un efecto, al efecto le falta el cleanup. Escribe la función de cleanup en vez de defenderte de la segunda ejecución.
Errores comunes con las dependencias de useEffect
Un objeto o una función literal recreados en cada render. Dos objetos con el mismo contenido no son el mismo objeto:
| |
Pon un objeto así en un array de dependencias y el efecto ve un valor “nuevo” en cada render, haya cambiado algo que te importe o no:
| |
Construye el objeto dentro del efecto en su lugar, y depende de los valores primitivos con los que está hecho:
| |
Leer el estado solo para calcular el siguiente estado. Un interval que lee count para incrementarlo obliga al efecto a depender de count, lo que recrea el interval en cada tick:
| |
Una actualización funcional elimina la lectura, y la dependencia se va con ella:
| |
Un efecto que hace dos tareas sin relación. Imagina un efecto que trae las ciudades de un país y las zonas de una ciudad. Con ambas en la misma función y el mismo array de dependencias, cambiar la ciudad vuelve a traer también las ciudades. Sepáralos: un efecto por cada pieza independiente de sincronización, cada uno con su propio array.
Cuándo no necesitas useEffect
Dos patrones parecen necesitar un efecto y no lo necesitan.
Resetear el estado cuando una prop cambia. Limpiar el estado local de un formulario cuando userId cambia es tentador escribirlo como un efecto que observa userId. Funciona, pero el componente renderiza una vez con el estado viejo antes de que el reset llegue. Pasa el valor que cambia como key del componente en su lugar, y React lo trata como una instancia nueva, sin ningún efecto de por medio:
| |
Algo que pasa por un clic, no por un render. Una notificación de “añadido al carrito” que observa product.isInCart en un efecto se dispara otra vez en cada recarga de la página donde el artículo ya está en el carrito, porque el efecto no puede distinguir “recién añadido” de “ya estaba”. La notificación pertenece al handler del clic que añade el artículo, donde se ejecuta solo cuando el usuario hace clic:
| |
Si algo debe pasar porque el usuario hizo algo, ponlo en el event handler de ese algo. useEffect es para lo que tiene que permanecer sincronizado mientras el componente esté en pantalla, sin importar lo que haga el usuario. Sincronizar una petición con el ciclo de vida del componente es ese tipo de problema (el lado de la petición se trata en manejo de errores de la Fetch API), igual que desconectar un observer al desmontar, como con la Intersection Observer API.
La próxima vez que un efecto se comporte mal, deja el array en paz al principio. Lista lo que lee el cuerpo de la función, decide si cada lectura necesita realmente ser reactiva, y comprueba si el código funciona sin ningún efecto. Arregla el cuerpo, y el array correcto sale de ahí. Si lo que te confunde es el orden de los callbacks asíncronos alrededor de un render, lee cómo funcionan async/await y el event loop, que cubre la call stack y la cola de microtasks.