useEffect Hook
Sincroniza un componente con un sistema externo ejecutando efectos secundarios después de la renderización.
Receta
Tarjeta de referencia rápida — lista para copiar y pegar.
// Ejecutar después de cada renderización
useEffect(() => {
console.log("renderizado");
});
// Ejecutar una sola vez al montar
useEffect(() => {
fetchData();
}, []);
// Ejecutar cuando las dependencias cambian
useEffect(() => {
document.title = `Hiciste clic ${count} veces`;
}, [count]);
// Limpiar al desmontar o antes de reintentar
useEffect(() => {
const id = setInterval(tick, 1000);
return () => clearInterval(id);
}, []);Cuándo recurrir a esto: Necesitas sincronizar con algo fuera de React — obtener datos, suscribirse a eventos, manipular el DOM o configurar temporizadores.
Ejemplo funcional
"use client";
import { useEffect, useState } from "react";
export function WindowSize() {
const [size, setSize] = useState({ width: 0, height: 0 });
useEffect(() => {
function handleResize() {
setSize({ width: window.innerWidth, height: window.innerHeight });
}
handleResize(); // Establece el tamaño inicial
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
return (
<p className="text-sm font-mono">
Ventana: {size.width} × {size.height}
</p>
);
}Lo que esto demuestra:
- Suscribirse a un evento del navegador (
resize) dentro deuseEffect - Devolver una función de limpieza para remover el listener al desmontar
- Array de dependencias vacío
[]para que el efecto se ejecute solo una vez al montar - Establecer el estado inicial dentro del efecto para evitar desajuste de hidratación en SSR
Análisis profundo
Cómo funciona
useEffectse ejecuta después de que el navegador pinte la pantalla, no durante la renderización- React llama a tu función de configuración después de que el componente monta y después de cada rerenderización donde las dependencias cambiaron
- Antes de reintentar el efecto, React llama a la función de limpieza anterior (si se proporciona)
- Al desmontar, React llama a la función de limpieza una última vez
- En Strict Mode durante el desarrollo, React monta, desmonta y remonta cada componente para exponer lógica de limpieza faltante
Parámetros y valores de retorno
| Parámetro | Tipo | Descripción |
|---|---|---|
setup | () => void o () => () => void | Función de efecto; opcionalmente devuelve una función de limpieza |
dependencies | unknown[] u omitido | Array de valores reactivos de los que depende el efecto |
| Retorno | Tipo | Descripción |
|---|---|---|
| (ninguno) | void | useEffect no devuelve un valor |
Variaciones
Obtención de datos con limpieza (controlador de aborto):
useEffect(() => {
const controller = new AbortController();
async function load() {
const res = await fetch(`/api/users/${id}`, {
signal: controller.signal,
});
const data = await res.json();
setUser(data);
}
load();
return () => controller.abort();
}, [id]);Conexión a un WebSocket:
useEffect(() => {
const ws = new WebSocket(url);
ws.onmessage = (e) => setMessages((prev) => [...prev, JSON.parse(e.data)]);
return () => ws.close();
}, [url]);Sincronizar con localStorage:
useEffect(() => {
localStorage.setItem("theme", theme);
}, [theme]);Notas de TypeScript
// Efecto con async — debe envolver porque useEffect no puede devolver una Promise
useEffect(() => {
async function fetchData() {
const res = await fetch("/api/data");
const json: DataType = await res.json();
setData(json);
}
fetchData();
}, []);
// TypeScript detecta automáticamente tipos de retorno de limpieza faltantesTrampas
-
Bucle infinito — Actualizar estado que también es una dependencia causa que el efecto se reintenté sin fin. Solución: Reduce tu array de dependencias o mueve la actualización de estado detrás de una condición.
-
Dependencias faltantes — Omitir un valor del array de dependencias causa datos obsoletos dentro del efecto. Solución: Incluye todos los valores reactivos usados dentro del efecto, o refactoriza para remover la dependencia.
-
Async directamente en useEffect — Devolver una
Promiseen lugar de una función de limpieza produce una advertencia de React. Solución: Define una funciónasyncdentro del efecto y llámala inmediatamente. -
Condiciones de carrera en rerenderizaciones rápidas — Si
idcambia rápidamente, un fetch anterior puede resolverse después de uno más nuevo y sobrescribir datos correctos. Solución: Usa unAbortControllero una bandera booleanaignoreen la limpieza. -
Ejecutar en el servidor —
useEffectno se ejecuta durante SSR. Acceder awindowodocumentsin guardar causa errores de compilación. Solución: Accede a APIs del navegador solo dentro deuseEffecto detrás de un chequeotypeof window !== "undefined".
Alternativas
| Alternativa | Usar cuando | No usar cuando |
|---|---|---|
useLayoutEffect | Necesitas medir o mutar el DOM antes de que el navegador pinte | El efecto no toca el layout o el DOM visual |
| Manejador de eventos | El efecto secundario es disparado por una acción específica del usuario (clic, envío) | El efecto debe ejecutarse al montar o cuando un valor cambia |
fetch de Server Component | Los datos pueden obtenerse en tiempo de solicitud en el servidor | Los datos dependen del estado del lado del cliente o interacción del usuario |
useSyncExternalStore | Suscribirse a una tienda externa (Redux, API del navegador) | Configuración única u obtención de datos |
¿Por qué no obtener datos en useEffect? Para muchas aplicaciones, marcos como Next.js ofrecen obtención de datos del lado del servidor que evita spinners de carga y cascadas. Reserva la obtención de datos en useEffect para necesidades verdaderamente solo del cliente.
Ejemplo del mundo real
De una aplicación SaaS de producción Next.js 15 / React 19 (SystemsArchitect.io).
// Ejemplo de producción: Navegación por teclado para tarjetas de estudio
// Archivo: src/hooks/use-card-keyboard.ts
import { useEffect } from "react";
interface UseCardKeyboardOptions {
isAutoplayOn: boolean;
cardNumber: number;
totalCards: number;
hasMoreCards: boolean;
onCardClick: () => void;
onNext: () => void;
onPrev: () => void;
}
export function useCardKeyboard({
isAutoplayOn, cardNumber, totalCards, hasMoreCards,
onCardClick, onNext, onPrev,
}: UseCardKeyboardOptions) {
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (isAutoplayOn) return;
if (event.code === "Space" || event.key === " ") {
event.preventDefault();
onCardClick();
} else if (event.key === "ArrowLeft" || event.key === "a") {
event.preventDefault();
if (cardNumber > 1) onPrev();
} else if (event.key === "ArrowRight" || event.key === "d") {
event.preventDefault();
if (cardNumber < totalCards || hasMoreCards) onNext();
}
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [cardNumber, totalCards, hasMoreCards, isAutoplayOn, onCardClick, onNext, onPrev]);
}Lo que esto demuestra en producción:
- La limpieza remueve el listener
keydownal desmontar y cuando las dependencias cambian, previniendo que se acumulen manejadores obsoletos. - Cada valor referenciado dentro del efecto está incluido en el array de dependencias. Faltar incluso uno (como
cardNumber) causaría bugs de cierre obsoleto donde se capturan valores antiguos. - Los callbacks
onCardClick,onNextyonPrevdeben envolverse enuseCallbackpor el componente padre. Sin eso, cada rerenderización padre crea nuevas referencias de función, causando que este efecto se desuscriba y se resuscriba el listener innecesariamente. - La guardia
isAutoplayOnen la parte superior del manejador es un patrón de producción para deshabilitar la entrada de teclado durante la reproducción automatizada sin remover completamente el listener. - Extraer esto en un hook personalizado mantiene el componente limpio y hace que el comportamiento del teclado sea independientemente testeable.
Preguntas frecuentes
¿Cuál es la diferencia entre un array de dependencias vacío [] y ningún array de dependencias?
useEffect(() => { ... }, [])se ejecuta una sola vez al montar y se limpia al desmontar.useEffect(() => { ... })se ejecuta después de cada renderización.- Omitir el array es raramente intencional y a menudo causa problemas de rendimiento.
¿Por qué no puedo pasar una función async directamente a useEffect?
useEffectespera que el callback devuelvaundefinedo una función de limpieza.- Una función
asyncdevuelve unaPromise, que React no puede usar como función de limpieza. - Envuelve la lógica async en una función interna y llámala inmediatamente:
useEffect(() => {
async function fetchData() {
const res = await fetch("/api/data");
setData(await res.json());
}
fetchData();
}, []);¿Cómo prevengo condiciones de carrera al obtener datos en useEffect?
- Usa un
AbortControllerpara cancelar la solicitud anterior cuando cambian las dependencias. - Alternativamente, usa una bandera booleana
ignoreestablecida entrueen la función de limpieza.
useEffect(() => {
const controller = new AbortController();
fetch(`/api/users/${id}`, { signal: controller.signal })
.then(res => res.json())
.then(setUser);
return () => controller.abort();
}, [id]);Trampa: ¿Por qué mi useEffect se ejecuta dos veces en desarrollo?
- En React 18+ Strict Mode, React monta, desmonta y remonta cada componente durante el desarrollo.
- Esto es intencional — expone lógica de limpieza faltante (event listeners, suscripciones, temporizadores).
- No sucede en builds de producción.
¿Cuándo debo usar useEffect versus un manejador de eventos para efectos secundarios?
- Usa un manejador de eventos cuando el efecto secundario es disparado por una acción específica del usuario (clic, envío, pulsación de tecla).
- Usa
useEffectcuando necesitas sincronizar con un sistema externo al montar o cuando una dependencia cambia. - Los manejadores de eventos son más simples y evitan bugs relacionados con dependencias.
¿Qué hace la función de limpieza y cuándo se ejecuta?
- La función de limpieza se ejecuta antes de que el efecto se reintenté (cuando las dependencias cambian) y una sola vez cuando el componente se desmonta.
- Se usa para desuscribirse de eventos, limpiar temporizadores o abortar fetches.
- Si olvidas la limpieza, te arriesgas a fugas de memoria y manejadores obsoletos.
Trampa: ¿Por qué agregar un objeto como dependencia causa que mi efecto se ejecute en cada renderización?
- React compara dependencias con
Object.is, que verifica igualdad de referencia. - Un nuevo literal de objeto
{}o array[]creado durante la renderización tiene una nueva referencia cada vez. - Solución memoizando el objeto con
useMemoo extrayendo dependencias primitivas.
¿Cómo escribo la función de limpieza en TypeScript?
useEffect((): (() => void) => {
const id = setInterval(tick, 1000);
return () => clearInterval(id);
}, []);
// TypeScript infiere el tipo de limpieza automáticamente
// en la mayoría de casos — la tipificación explícita rara vez es necesaria.¿Cómo accedo a window o document de forma segura dentro de useEffect con SSR?
useEffectsolo se ejecuta en el cliente, por lo quewindowydocumentsiempre están disponibles dentro de él.- El peligro es acceder a ellos fuera de
useEffectdurante renderización del lado del servidor. - Mantén todo acceso a API del navegador dentro de
useEffecto detrás de una guardiatypeof window !== "undefined".
¿Debo dividir un gran useEffect en múltiples más pequeños?
- Sí, si el efecto maneja preocupaciones no relacionadas (p. ej., obtener datos y suscribirse a eventos de redimensionamiento).
- Cada efecto debe tener su propio array de dependencias que coincida con su preocupación específica.
- Esto hace que la limpieza y la gestión de dependencias sea más simple y menos propensa a errores.
¿En qué se diferencia useEffect de useLayoutEffect?
useEffectse ejecuta después de que el navegador pinta la pantalla (asincrónico).useLayoutEffectse ejecuta sincrónico después de mutaciones del DOM pero antes de que el navegador pinte.- Usa
useLayoutEffectsolo cuando necesites medir o mutar el DOM antes de que el usuario lo vea (p. ej., tooltips, animaciones).
Relacionado
- useState — estado que tus efectos leen y escriben
- useRef — valores mutables que persisten a través de renderizaciones sin disparar efectos
- useCallback — estabilizar funciones usadas como dependencias de efectos
- Custom Hooks — extraer lógica de efectos reutilizables en hooks personalizados