6 Técnicas Secretas del useEffect
Más allá del fetch y el event listener. Lo que la documentación muestra — y lo que no explica bien.

useEffect es el hook más enseñado. Y el más malentendido. No porque la API sea mala — sino porque la mayoría de los tutoriales se detienen en el segundo ejemplo.
Fetch al montar. Event listener. Array de dependencias con la variable correcta. Eso es lo básico, y lo básico está en todos lados.
Lo que no está en todos lados son los seis comportamientos que separan a quienes usan useEffect de quienes lo entienden.
Técnica 01 — Cleanup con AbortController
Cada vez que un componente se desmonta antes de que el fetch termine, la respuesta igual llega — e intenta actualizar el estado de un componente que ya no existe.
React mostraba el aviso en consola: "Can't perform a React state update on an unmounted component". El aviso fue eliminado en React 18. El bug continúa.
function UserProfile({ userId }) { const [user, setUser] = useState(null); useEffect(() => { const controller = new AbortController(); async function fetchUser() { try { const res = await fetch(`/api/users/${userId}`, { signal: controller.signal, }); const data = await res.json(); setUser(data); } catch (err) { if (err.name === 'AbortError') return; console.error(err); } } fetchUser(); return () => controller.abort(); }, [userId]); return <div>{user?.name}</div>;}Técnica 02 — El array de dependencias es una declaración, no un filtro
El instinto equivocado aparece así: el efecto corre demasiado, entonces el dev elimina una dependencia del array para "detener" las ejecuciones extra. Funciona hasta el día que no funciona.
El array de dependencias le dice a React qué valores usa este efecto. No qué valores deben activarlo. Esa distinción lo cambia todo. Omitir una dependencia no silencia el re-run — crea un stale closure: el efecto queda atrapado en un snapshot antiguo del valor, y el bug aparece de forma no determinística.
useEffect(() => {
// count se lee aquí,
// pero fue omitido del array.
// El valor nunca se actualiza.
document.title = `Count: ${count}`;
}, []); // ← mintiendo a React
useEffect(() => {
// count está en el array.
// React sabe cuándo re-ejecutar.
document.title = `Count: ${count}`;
}, [count]); // ← contrato honesto
Si el efecto corre demasiado, el problema real generalmente es un objeto o función que se recrea en cada render. La solución es estabilizar los valores — con useCallback, useMemo, o moviendo la definición dentro del propio efecto. No omitir.

Técnica 03 — useLayoutEffect: cuando el DOM no puede esperar
useEffect dispara después de que el navegador pinta. En la mayoría de los casos, es exactamente lo que quieres. Pero existe una ventana entre que React confirma los cambios en el DOM y el navegador pinta la pantalla.
En posicionamiento de tooltips, mediciones de layout y animaciones que dependen del tamaño real — useEffect dispara demasiado tarde. El usuario ve el elemento en la posición incorrecta antes de que se corrija.
function Tooltip({ targetRef, children }) { const tooltipRef = useRef(null); const [pos, setPos] = useState({ top: 0, left: 0 }); // Mide ANTES de pintar — sin flash de posición incorrecta useLayoutEffect(() => { if (!targetRef.current || !tooltipRef.current) return; const targetRect = targetRef.current.getBoundingClientRect(); const tooltipRect = tooltipRef.current.getBoundingClientRect(); setPos({ top: targetRect.top - tooltipRect.height - 8, left: targetRect.left + targetRect.width / 2 - tooltipRect.width / 2, }); }, [targetRef]); return ( <div ref={tooltipRef} style={{ position: 'fixed', top: pos.top, left: pos.left }}> {children} </div> );}Técnica 04 — Refs como válvula de escape de dependencias
Las funciones de callback llegan como props. Cuando necesitas llamarlas dentro de un efecto, el linter exige que entren en el array de dependencias. Pero los callbacks recreados en cada render hacen que el efecto se re-ejecute sin motivo real.
La ref es la salida: guarda el valor más reciente sin hacer que el efecto dependa de él.
function useInterval(callback, delay) { // Ref siempre con la versión más reciente del callback const savedCallback = useRef(callback); // Actualiza la ref sin re-ejecutar el efecto useEffect(() => { savedCallback.current = callback; }, [callback]); // El interval usa la ref — el array de deps solo necesita [delay] useEffect(() => { if (delay === null) return; const id = setInterval(() => { savedCallback.current(); // lee el valor más reciente via ref }, delay); return () => clearInterval(id); }, [delay]);}Técnica 05 — useEffectEvent: leer estado sin depender de él
Un efecto necesita reaccionar a un cambio — roomId — pero dentro de él, necesita leer un valor actual — theme — sin que ese valor cause re-ejecución. React 19 introdujo useEffectEvent para formalizar esta separación entre código reactivo y no-reactivo.
import { experimental_useEffectEvent as useEffectEvent } from 'react';function ChatRoom({ roomId, theme }) { // onConnected lee theme internamente, pero no depende de él const onConnected = useEffectEvent(() => { showNotification(`Conectado a la sala ${roomId}`, theme); }); useEffect(() => { const connection = createConnection(roomId); connection.on('connected', () => onConnected()); connection.connect(); return () => connection.disconnect(); }, [roomId]); // theme no necesita estar aquí}Técnica 06 — useEffect no gestiona estado derivado
Este es el error silencioso más común. El código funciona. Los tests pasan. Y aun así hay un render extra cada vez — más un useEffect que no debería existir.
// ❌ Effect para derivar estado — causa doble renderfunction FilteredList({ items, query }) { const [filtered, setFiltered] = useState([]); useEffect(() => { setFiltered(items.filter(item => item.name.includes(query))); }, [items, query]); return <List items={filtered} />;}// ✅ Calculado en el render — correcto y sin overheadfunction FilteredList({ items, query }) { const filtered = useMemo( () => items.filter(item => item.name.includes(query)), [items, query] ); return <List items={filtered} />;}useEffect existe para sincronizar React con sistemas externos: APIs, WebSockets, timers, localStorage, librerías de terceros. No para transformar datos que ya están dentro de React.
useEffect no ha cambiado mucho desde React 16.8. Lo que cambió es el entendimiento de lo que realmente es: una capa de sincronización entre React y el mundo externo.
Todo lo que no necesita el mundo externo — no pertenece ahí.
Pon a prueba tus conocimientos
Responde y valida tu comprensión.


