React
useEffect
Explica cómo useEffect sincroniza componentes con sistemas externos, cuándo se ejecuta, cómo declarar dependencias y por qué no sustituye la lógica de render.
- Última actualización
- Actualizada
- Nivel
- Aplicación
React
Explica cómo useEffect sincroniza componentes con sistemas externos, cuándo se ejecuta, cómo declarar dependencias y por qué no sustituye la lógica de render.
useEffect declara que un componente debe sincronizarse con un sistema externo después del commit. No es un lugar genérico para ejecutar cualquier código “después del render”.
React se ocupa de calcular y aplicar la interfaz. Un effect conecta ese árbol con algo que React no controla directamente:
render
↓
commit
↓
effect setup
↓
cambio de dependencia
↓
cleanup anterior
↓
nuevo setupimport { useEffect } from "react";
function ChatRoom({ roomId }: { roomId: string }) {
useEffect(() => {
const connection = createConnection(roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId]);
return <h1>Sala {roomId}</h1>;
}El setup establece la sincronización. El cleanup debe deshacerla.
Cuando roomId cambia:
roomId actual.Al desmontar el componente también ejecuta el cleanup final.
El componente debe poder renderizar sin depender de que el effect ya haya corrido. En client rendering, React aplica primero la interfaz y después ejecuta los effects normales.
Esto explica por qué no debes usar useEffect para calcular datos necesarios durante el mismo render:
function ProductList({ products }: { products: Product[] }) {
const [availableProducts, setAvailableProducts] = useState<Product[]>([]);
useEffect(() => {
setAvailableProducts(products.filter((product) => product.stock > 0));
}, [products]);
return <List products={availableProducts} />;
}Este diseño renderiza primero información obsoleta y después provoca otro render.
function ProductList({ products }: { products: Product[] }) {
const availableProducts = products.filter((product) => product.stock > 0);
return <List products={availableProducts} />;
}Si el cálculo es costoso, mide antes de considerar memoización.
Son reactivos los valores definidos dentro del componente y recibidos como props, porque pueden cambiar entre renders.
function SearchResults({ query }: { query: string }) {
const [page, setPage] = useState(1);
useEffect(() => {
synchronizeSearch({ query, page });
}, [query, page]);
}El array no es una lista de valores que eliges para controlar la frecuencia. Describe qué valores utiliza el effect.
React compara cada dependencia con su valor anterior mediante Object.is.
useEffect(() => {
// Runs after every committed render of this component
});useEffect(() => {
// Does not read changing reactive values
}, []);useEffect(() => {
// Re-synchronizes when roomId changes
}, [roomId]);Un array vacío no significa universalmente “una sola vez”. En desarrollo con Strict Mode React puede ejecutar setup, cleanup y setup al montar para comprobar que el effect es reversible. Además, desmontar y volver a montar crea una nueva instancia y un nuevo setup.
La regla de exhaustive dependencies detecta valores reactivos leídos dentro del effect.
useEffect(() => {
const connection = connect(serverUrl, roomId);
return () => connection.disconnect();
}, []); // Missing serverUrl and roomIdSuprimir la regla puede congelar valores antiguos dentro de la closure.
La solución no suele ser “engañar” al array, sino cambiar el diseño:
useEffectEvent para lógica no reactiva cuando corresponda.Cada render crea closures que capturan sus valores:
function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = window.setInterval(() => {
console.log(count);
}, 1000);
return () => window.clearInterval(id);
}, []);
return <button onClick={() => setCount((value) => value + 1)}>{count}</button>;
}El intervalo conserva el count del render inicial porque el effect declaró que no dependía de él.
Según la intención, puedes:
count como dependencia y recrear el intervalo.No existe una corrección universal; depende de qué debe reaccionar al cambio.
function NetworkStatus() {
const [isOnline, setIsOnline] = useState(() => navigator.onLine);
useEffect(() => {
function handleOnline() {
setIsOnline(true);
}
function handleOffline() {
setIsOnline(false);
}
window.addEventListener("online", handleOnline);
window.addEventListener("offline", handleOffline);
return () => {
window.removeEventListener("online", handleOnline);
window.removeEventListener("offline", handleOffline);
};
}, []);
return <p>{isOnline ? "Con conexión" : "Sin conexión"}</p>;
}Para una store externa compartida, useSyncExternalStore suele expresar mejor la suscripción y el snapshot.
function AutoSave({ draft }: { draft: Draft }) {
useEffect(() => {
const timeoutId = window.setTimeout(() => {
saveDraft(draft);
}, 800);
return () => window.clearTimeout(timeoutId);
}, [draft]);
return null;
}El cleanup cancela el timer anterior cuando cambia el borrador. Esto implementa un debounce de sincronización, no de render.
Un fetch manual dentro de un effect necesita considerar respuestas obsoletas:
function UserProfile({ userId }: { userId: string }) {
const [state, setState] = useState<
| { status: "loading" }
| { status: "success"; user: User }
| { status: "error"; message: string }
>({ status: "loading" });
useEffect(() => {
const controller = new AbortController();
async function loadUser() {
setState({ status: "loading" });
try {
const response = await fetch(`/api/users/${userId}`, {
signal: controller.signal,
});
if (!response.ok) {
throw new Error(`Request failed with ${response.status}`);
}
const user: User = await response.json();
setState({ status: "success", user });
} catch (error) {
if (controller.signal.aborted) return;
setState({
status: "error",
message: error instanceof Error ? error.message : "Unknown error",
});
}
}
void loadUser();
return () => controller.abort();
}, [userId]);
if (state.status === "loading") return <p>Loading user…</p>;
if (state.status === "error") return <p role="alert">{state.message}</p>;
return <h1>{state.user.name}</h1>;
}AbortController ayuda a detener el trabajo compatible y evita aplicar la respuesta cancelada.
Aun así, fetching manual en effects no resuelve por sí solo:
Framework loaders, Server Components o librerías de server state suelen ofrecer una estrategia más completa.
Un evento responde a una interacción específica:
function BuyButton({ product }: { product: Product }) {
async function handleBuy() {
await createOrder(product.id);
}
return <button onClick={handleBuy}>Comprar</button>;
}No conviertas la intención del usuario en state para observarla desde un effect:
// Avoid
useEffect(() => {
if (shouldBuy) createOrder(product.id);
}, [shouldBuy, product.id]);La compra pertenece al handler porque ocurre por una acción concreta.
Dos sistemas externos distintos suelen merecer effects distintos:
useEffect(() => {
const connection = connect(roomId);
return () => connection.disconnect();
}, [roomId]);
useEffect(() => {
document.title = `Room ${roomId}`;
}, [roomId]);No es por una regla de cantidad de líneas, sino porque sus ciclos de sincronización son independientes.
Evita usar effects para transformar state paso a paso:
useEffect(() => {
setTotal(items.reduce((sum, item) => sum + item.price, 0));
}, [items]);
useEffect(() => {
setHasDiscount(total > 100000);
}, [total]);Calcula ambos durante render:
const total = items.reduce((sum, item) => sum + item.price, 0);
const hasDiscount = total > 100000;Las cadenas añaden renders, estados intermedios y posibilidades de desincronización.
No necesitas un effect para resetear todo el state cuando cambia una entidad:
function ProfilePage({ userId }: { userId: string }) {
return <Profile key={userId} userId={userId} />;
}La key expresa que cada usuario corresponde a una identidad de componente distinta.
useLayoutEffect comparte el modelo setup/cleanup, pero se ejecuta antes de que el navegador pinte. Puede medir y ajustar layout sin mostrar un frame intermedio:
useLayoutEffect(() => {
const rect = ref.current?.getBoundingClientRect();
if (rect) setHeight(rect.height);
}, []);Bloquea paint, por lo que debe reservarse para trabajo visual que realmente necesita ocurrir antes de mostrar la pantalla.
En React 19.2, useEffectEvent es una API estable para extraer lógica de un effect que necesita leer valores actuales sin convertirlos en dependencias reactivas del ciclo principal.
No sirve para ocultar dependencias reales ni para reemplazar handlers normales. Su caso es separar una parte no reactiva dentro de una sincronización.
const onConnected = useEffectEvent(() => {
showNotification("Connected", theme);
});
useEffect(() => {
const connection = connect(roomId);
connection.on("connected", onConnected);
return () => connection.disconnect();
}, [roomId]);La conexión reacciona a roomId; la notificación puede leer el tema actual sin reconectar por cada cambio visual.
Los effects no se ejecutan durante server rendering. Solo comienzan cuando el componente correspondiente se monta e hidrata en el cliente.
Por eso:
window debe ocurrir en código cliente y en una fase segura.Úsalo cuando puedas completar esta frase:
Este componente debe mantenerse sincronizado con __.
Ejemplos:
No necesitas un effect para:
useEffect como lifecycle genérico de function components.[] aunque se lean props o state.filteredItems en un effect es normalmente innecesario?useLayoutEffect?Dependencias reactivas de useEffect profundiza en closures, identidad, funciones, objetos y estrategias para corregir dependencias sin engañar al linter.