useEffect por dentro: cuándo corre y por qué
¿En qué momento exacto corre mi efecto, y cuándo su cleanup?
Al terminar vas a poder explicar
- Ubicar el efecto y su cleanup en la línea de tiempo de un render
- Explicar qué compara React en el array de dependencias y con qué operador
- Diagnosticar un bucle infinito de efectos leyendo la traza
«useEffect es código que corre después del render» es una definición lo bastante vaga como para permitirte escribir bugs con la conciencia tranquila. Vamos a apretarla.
Llamar a useEffect no ejecuta nada. Guarda un objeto { create, destroy, deps } en el hook, marca el fiber con un flag, y encola el efecto. React lo va a vaciar en un momento muy preciso.
Dónde cae exactamente en la línea de tiempo
- Fase render — se ejecuta tu función y el efecto queda agendado.
- Fase commit — se aplican los cambios al DOM.
- Corren los
useLayoutEffect. - El navegador pinta.
- Corren los
useEffect, en una tarea aparte.
Que estén después del pintado tiene una consecuencia directa: no bloquean la actualización visual. También significa que hay un frame en el que la pantalla ya se actualizó pero tu efecto todavía no corrió. Si en ese efecto medís y ajustás algo del layout, el usuario ve el salto. Para eso está useLayoutEffect, que vemos en la próxima lección.
Las dependencias se comparan con Object.is
En cada render, React compara el array de deps nuevo contra el guardado, elemento por elemento, con Object.is. Superficial. Por identidad.
- Todos iguales — el efecto no corre, el cleanup no corre, y la función nueva que le pasaste se descarta.
- Alguno distinto — corre el cleanup del efecto anterior y después el efecto nuevo. Siempre en ese orden.
- Sin array — corre después de cada render, sin excepción.
- Array vacío — corre una sola vez, en el montaje.
Deps iguales contra deps cambiadas, en la misma corrida
AGENDADOpaso 1 / 179Sin hooks en este paso.
Árbol de fibers
- <HostRoot>
Salida
Nada en el host todavía. React no commiteó ningún cambio.
createRoot().render()
Montaje inicial. No hay árbol previo, así que todo se va a crear desde cero.
VER EL CÓDIGO QUE SE ESTÁ TRAZANDO
1function ConEfecto() {2 const [n, setN] = useState(0);3 const [otro, setOtro] = useState("a");4 5 useEffect(() => {6 conectar(n);7 return () => desconectar(n);8 }, [n]); // sólo n está en las deps9 10 return (11 <div>12 <button onClick={() => setN(v => v + 1)}>n = {n}</button>13 <button onClick={() => setOtro("b")}>otro = {otro}</button>14 </div>15 );16}Con el foco acá: ← → un paso · espacio reproduce · Inicio/Fin van a los extremos.
En esa traza hay dos interacciones. La primera cambia un estado que no está en las deps: buscá areHookInputsEqual() → true. El efecto ni se inmuta. La segunda sí cambia una dep, y ahí aparece la secuencia completa: cleanup primero, efecto después.
1function Chat({ sala }) {2 const opciones = { servidor: "wss://chat" }; // objeto NUEVO en cada render3 4 useEffect(() => {5 conectar(sala, opciones);6 }, [sala, opciones]); // ✗ opciones nunca es igual: reconecta siempre7}Las salidas: mover el objeto fuera del componente si es constante, construirlo dentro del efecto, o — si de verdad depende de props — envolverlo en useMemo. En ese orden de preferencia.
El cleanup no es opcional
Lo que devuelve tu efecto se guarda como destroy y React lo llama antes del próximo efecto y al desmontar. Si tu efecto crea algo que persiste — una suscripción, un intervalo, un listener, una conexión — sin cleanup tenés una fuga.
El orden cleanup → create es lo que hace que un efecto sea idempotente: montar y desmontar diez veces tiene que dejar el sistema como estaba. Por eso StrictMode en desarrollo monta, desmonta y vuelve a montar cada componente: si tu efecto no sobrevive a ese ciclo, le falta el cleanup.
El bucle infinito, desde adentro
Un efecto sin deps que llama a setState
AGENDADOpaso 1 / 1200Sin hooks en este paso.
Árbol de fibers
- <HostRoot>
Salida
Nada en el host todavía. React no commiteó ningún cambio.
createRoot().render()
Montaje inicial. No hay árbol previo, así que todo se va a crear desde cero.
La corrida se detuvo: Bucle infinito: el render disparó otro render más de 50 veces seguidas. Casi siempre es un setState dentro del cuerpo del componente o un useEffect sin array de dependencias.
VER EL CÓDIGO QUE SE ESTÁ TRAZANDO
1function BucleInfinito() {2 const [n, setN] = useState(0);3 4 useEffect(() => {5 setN(v => v + 1); // ⚠️ sin array de deps6 }); // ⚠️ corre después de CADA render7 8 return <span>n = {n}</span>;9}Con el foco acá: ← → un paso · espacio reproduce · Inicio/Fin van a los extremos.
El efecto corre, llama a setState, eso agenda un render, el render vuelve a agendar el efecto — porque sin array de deps siempre está pendiente — y arranca de nuevo.
React corta a los 50 renders anidados con un error. Es el mismo NESTED_UPDATE_LIMIT que usa React de verdad. Sin ese límite, la pestaña se te cuelga.
Antes de seguir · predecí
Este efecto tiene que correr sólo cuando cambia el usuario, pero corre en cada render. ¿Por qué?
1function Panel({ usuarioId }) {2 const [datos, setDatos] = useState(null);3 4 const cargar = () => traerDatos(usuarioId);5 6 useEffect(() => {7 cargar().then(setDatos);8 }, [cargar]);9}Lo que te llevás
Un efecto es una entrada en una cola que se vacía después del pintado. Las deps se comparan con Object.is, así que objetos y funciones inline las rompen. El cleanup corre antes del efecto nuevo y al desmontar. Y un efecto sin deps que setea estado es un bucle infinito con pasos extra.
Antes de marcarla, comprobá
- ¿Podés explicar ubicar el efecto y su cleanup en la línea de tiempo de un render?
- ¿Podés explicar explicar qué compara React en el array de dependencias y con qué operador?
- ¿Podés explicar diagnosticar un bucle infinito de efectos leyendo la traza?