Busca en todas las páginas de la documentación
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
import { useState, useEffect, useRef, useCallback } from "react";
/**
* useThrottledValue
* Devuelve una copia throttled de `value` que se actualiza como máximo
* una vez cada `interval` ms.
*/
function useThrottledValue<T>(value: T, interval: number): T {
const [throttled, setThrottled] = useState(value);
const lastUpdated = useRef(Date.now());
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
useEffect(() => {
const now = Date.now();
const elapsed = now - lastUpdated.current;
if (elapsed >= interval) {
setThrottled(value);
lastUpdated.current = now;
} else {
// Programa una actualización final
if (timerRef.current) clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => {
setThrottled(value);
lastUpdated.current = Date.now();
}, interval - elapsed);
}
return () => {
if (timerRef.current) clearTimeout(timerRef.current);
};
}, [value, interval]);
return throttled;
}
/**
* useThrottledCallback
* Devuelve una versión stable, throttled de `callback` que
* se ejecuta como máximo una vez cada `interval` ms.
*/
function useThrottledCallback<T extends (...args: any[]) => void>(
callback: T,
interval: number
): T & { cancel: () => void } {
const callbackRef = useRef(callback);
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const lastCalledRef = useRef(0);
const lastArgsRef = useRef<Parameters<T> | null>(null);
useEffect(() => {
callbackRef.current = callback;
}, [callback]);
useEffect(() => {
return () => {
if (timerRef.current) clearTimeout(timerRef.current);
};
}, []);
const cancel = useCallback(() => {
if (timerRef.current) clearTimeout(timerRef.current);
timerRef.current = null;
lastArgsRef.current = null;
}, []);
const throttled = useCallback(
(...args: Parameters<T>) => {
lastArgsRef.current = args;
const now = Date.now();
const elapsed = now - lastCalledRef.current;
if (elapsed >= interval) {
callbackRef.current(...args);
lastCalledRef.current = now;
} else if (!timerRef.current) {
timerRef.current = setTimeout(() => {
if (lastArgsRef.current) {
callbackRef.current(...lastArgsRef.current);
}
lastCalledRef.current = Date.now();
timerRef.current = null;
lastArgsRef.current = null;
}, interval - elapsed);
}
},
[interval]
) as T & { cancel: () => void };
throttled.cancel = cancel;
return throttled;
}Cuándo usarlo: Necesitas actualizaciones consistentes y espaciadas durante eventos continuos como scrolling, resizing o movimiento del mouse, en lugar de esperar a que el evento se detenga (eso sería debounce).
"use client";
import { useState, useEffect, useRef } from "react";
function ScrollTracker() {
const [scrollY, setScrollY] = useState(0);
const handleScroll = useThrottledCallback(() => {
setScrollY(window.scrollY);
}, 100);
useEffect(() => {
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, [handleScroll]);
return (
<div style={{ position: "fixed", top: 10, right: 10 }}>
Scroll: {scrollY}px
</div>
);
}
function ResizeDisplay() {
const [width, setWidth] = useState(
typeof window !== "undefined" ? window.innerWidth : 0
);
const throttledWidth = useThrottledValue(width, 200);
useEffect(() => {
const handler = () => setWidth(window.innerWidth);
window.addEventListener("resize", handler);
return () => window.removeEventListener("resize", handler);
}, []);
return (
<p>
Raw: {width}px - Throttled: {throttledWidth}px
</p>
);
}Qué demuestra esto:
useThrottledCallback dispara el scroll handler como máximo cada 100 ms, manteniendo la UI responsiva sin inundar actualizaciones de stateuseThrottledValue suaviza los valores raw de resize para actualizarse como máximo cada 200 ms| Comportamiento | Debounce | Throttle |
|---|---|---|
| Cuándo se dispara | Después de que la entrada se detiene por N ms | Como máximo una vez cada N ms |
| Mejor para | Input de búsqueda, validación de formularios | Scroll, resize, drag |
| Responsividad | Se siente demorado | Se siente smooth |
| Valor final | Solo el último | Inicial + final |
| Parámetro | Tipo | Predeterminado | Descripción |
|---|---|---|---|
value | T | - | El valor a throttle |
interval | number | - | Mínimo de ms entre actualizaciones |
| Devuelve | T | - | El valor throttled |
| Parámetro | Tipo | Predeterminado | Descripción |
|---|---|---|---|
callback | (...args) => void | - | Función a throttle |
interval | number | - | Mínimo de ms entre llamadas |
| Devuelve | T & \{ cancel \} | - | Función throttled con cancel |
Throttle solo inicial: Salta la llamada final si solo quieres el primer evento en cada ventana. Elimina la rama de setTimeout final.
Throttle con requestAnimationFrame: Para actualizaciones visuales, reemplaza el timer con requestAnimationFrame para throttling perfecto de frame de 16 ms:
function useRAFCallback(callback: () => void) {
const rafRef = useRef(0);
const callbackRef = useRef(callback);
callbackRef.current = callback;
return useCallback(() => {
cancelAnimationFrame(rafRef.current);
rafRef.current = requestAnimationFrame(() => callbackRef.current());
}, []);
}T en el value hook preserva el tipo de entrada.Parameters<T> en el callback hook preserva los tipos de argumentos.T & \{ cancel \} agrega métodos de control sin perder la firma original.interval por debajo de 16 ms no proporciona beneficio ya que el navegador no puede renderizar más rápido que un frame. Solución: Usa 16 ms mínimo, o cambia a requestAnimationFrame.setTimeout no es perfectamente preciso. Para código crítico en animaciones, Solución: usa requestAnimationFrame en su lugar.window durante server render lanza un error. Solución: Protege con typeof window !== "undefined" o inicializa a un valor seguro.| Paquete | Nombre del hook | Notas |
|---|---|---|
usehooks-ts | useThrottle | Throttle de valor solamente |
ahooks | useThrottle, useThrottleFn | Con todas las características, configuración inicial/final |
@uidotdev/usehooks | useThrottle | Throttle de valor mínimo |
lodash | _.throttle | No es un hook; envuelve en useRef |
use-debounce | useThrottledCallback | Parte del paquete debounce |
useThrottledValue acepta un valor reactivo y devuelve una copia throttled que se actualiza como máximo una vez por intervalo.useThrottledCallback acepta una función y devuelve una versión throttled de esa función.Almacenar el callback en callbackRef asegura que el timeout siempre llame a la versión más reciente de la función. Sin él, el closure capuraría un callback obsoleto de un render anterior.
setTimeout también tiene un retraso mínimo de ~4 ms en la mayoría de navegadores.requestAnimationFrame en su lugar.Llama al método .cancel() en la función devuelta:
const throttled = useThrottledCallback(handler, 200);
// Después:
throttled.cancel();No. Ambos hooks incluyen una llamada final vía setTimeout. Cuando el burst termina, el timer final se dispara con el valor o argumentos más recientes, asegurando que nada se descarte.
window durante server-side rendering lanza un ReferenceError.typeof window !== "undefined" antes de leer window.scrollY o adjuntar listeners.useEffect en el ejemplo en funcionamiento solo se ejecuta en el cliente, pero el estado inicial también debe ser seguro.Sin la llamada final, solo se dispara la invocación inicial. Si el valor cambia durante la ventana de cooldown, la actualización final se descarta silenciosamente. Siempre mantén la rama final para capturar el último valor.
requestAnimationFrame se dispara una vez por display frame (~16 ms a 60 Hz).La firma genérica T extends (...args: any[]) => void captura el tipo de función original. Parameters<T> extrae la tupla de argumentos, así que el wrapper throttled acepta los mismos parámetros que el original.
Combina el tipo de función original T con un objeto que tiene un método cancel. Esto significa que la función devuelta es callable con la misma firma que la original, y también expone .cancel() para limpieza.
Revisado por Chris St. John·Última actualización: 16 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥