Frontend

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.

6 Técnicas Secretas del useEffect

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.

Bugs evitados
6
patrones de error recurrentes
React 19
1
técnica del futuro, disponible hoy
Re-renders
0
innecesarios con Ref como escape

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.

MONTA useEffect dispara fetch() DESMONTA ¿cleanup corrió? respuesta llega setState() 💥 SIN abort → bug silencioso CON AbortController → cancelado ✓
Diagrama — Ciclo de vida con fetch asíncrono
JS
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.

❌ Stale closure

useEffect(() => {
// count se lee aquí,
// pero fue omitido del array.
// El valor nunca se actualiza.
document.title = `Count: ${count}`;
}, []); // ← mintiendo a React

✅ Declaración honesta

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.

TIEMPO → useEffect React render DOM commit Pintura useEffect dispara useLayoutEffect React render DOM commit useLayoutEffect Pintura flash visible aquí ↑ síncrono, antes de pintar ✓ ⚠ useLayoutEffect bloquea la pintura. No hagas operaciones lentas aquí.
Diagrama — Timing: useEffect vs useLayoutEffect
JS
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.

A) Dependencia directa — re-runs innecesarios Render 1 Render 2 Render 3 re-run re-run ... y así sucesivamente B) Via ref — efecto estable Render 1 Render 2 Render 3 callbackRef siempre actualizado efecto: corre 1x ✓
Diagrama — Callback via ref vs. dependencia directa
JS
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.

JS
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í}
useEffect reacciona a: [roomId] conectar / desconectar reactivo por naturaleza llama useEffectEvent lee: theme (siempre actual) showNotification() no-reactivo — no causa re-run ✓
Diagrama — Separación reactivo / no-reactivo

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.

JS
// ❌ 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.

useState + useEffect Render 1 lista desactualizada Render 2 lista correcta (ahora) = 2 renders por query change useMemo en el render Render único — correcto ✓ = 1 render por query change
Diagrama — Cantidad de renders

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í.

Quiz interactivo

Pon a prueba tus conocimientos

Responde y valida tu comprensión.

1. ¿Qué API recomienda el artículo para cancelar peticiones en el cleanup de useEffect?
2. Según el artículo, el array de dependencias es...
3. ¿Cuándo usar useLayoutEffect en lugar de useEffect?