Busca en todas las páginas de la documentación
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Dos hooks complementarios: useDebouncedValue retrasa un valor reactivo; useDebouncedCallback retrasa una llamada de función.
import { useState, useEffect, useRef, useCallback, useMemo } from "react";
/**
* useDebouncedValue
* Devuelve una copia retrasada de `value` que solo se actualiza
* después de `delay` ms de inactividad.
*/
function useDebouncedValue<T>(value: T, delay: number): T {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const id = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(id);
}, [value, delay]);
return debounced;
}
/**
* useDebouncedCallback
* Devuelve una versión estable retrasada de `callback`.
* Soporta invocación en el borde inicial a través de `options.leading`.
*/
function useDebouncedCallback<T extends (...args: any[]) => void>(
callback: T,
delay: number,
options: { leading?: boolean } = {}
): T & { cancel: () => void; flush: () => void } {
const { leading = false } = options;
const callbackRef = useRef(callback);
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const lastArgsRef = useRef<Parameters<T> | null>(null);
const leadingFiredRef = useRef(false);
// Siempre mantener el callback más reciente
useEffect(() => {
callbackRef.current = callback;
}, [callback]);
// Limpieza al desmontar
useEffect(() => {
return () => {
if (timerRef.current) clearTimeout(timerRef.current);
};
}, []);
const cancel = useCallback(() => {
if (timerRef.current) clearTimeout(timerRef.current);
timerRef.current = null;
lastArgsRef.current = null;
leadingFiredRef.current = false;
}, []);
const flush = useCallback(() => {
if (lastArgsRef.current) {
callbackRef.current(...lastArgsRef.current);
}
cancel();
}, [cancel]);
const debounced = useCallback(
(...args: Parameters<T>) => {
lastArgsRef.current = args;
if (leading && !leadingFiredRef.current) {
leadingFiredRef.current = true;
callbackRef.current(...args);
}
if (timerRef.current) clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => {
if (!leading) {
callbackRef.current(...args);
}
leadingFiredRef.current = false;
timerRef.current = null;
lastArgsRef.current = null;
}, delay);
},
[delay, leading]
) as T & { cancel: () => void; flush: () => void };
debounced.cancel = cancel;
debounced.flush = flush;
return debounced;
}Cuándo usarlo: Necesitas evitar hacer demasiadas solicitudes a una API en cada pulsación de tecla (búsqueda mientras escribes), o quieres agrupar eventos rápidos como resize o scroll en una única actualización.
"use client";
import { useState } from "react";
function SearchInput() {
const [query, setQuery] = useState("");
const debouncedQuery = useDebouncedValue(query, 300);
// Solo dispara solicitud de red cuando debouncedQuery cambia
useEffect(() => {
if (!debouncedQuery) return;
fetch(`/api/search?q=${encodeURIComponent(debouncedQuery)}`)
.then((res) => res.json())
.then((data) => console.log(data));
}, [debouncedQuery]);
return (
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Buscar..."
/>
);
}
function SaveButton() {
const debouncedSave = useDebouncedCallback(
(content: string) => {
fetch("/api/save", {
method: "POST",
body: JSON.stringify({ content }),
});
},
1000,
{ leading: true }
);
return (
<button onClick={() => debouncedSave("draft content")}>
Guardar (retrasado)
</button>
);
}Lo que demuestra esto:
useDebouncedValue retrasa la consulta de búsqueda para que la API se llame solo después de que el usuario deje de escribir durante 300 msuseDebouncedCallback con leading: true dispara inmediatamente al primer clic, luego ignora clics rápidos durante 1 segundosetTimeout cada vez que value cambia. La función de limpieza en useEffect borra el timer anterior, por lo que solo la última actualización dentro de delay ms realmente se registra en el state.cancel() borra cualquier invocación pendiente. flush() invoca el callback pendiente inmediatamente y reinicia el state.| Parámetro | Tipo | Predeterminado | Descripción |
|---|---|---|---|
value | T | - | El valor a retrasar |
delay | number | - | Milisegundos a esperar |
| Devuelve | T | - | El valor retrasado |
| Parámetro | Tipo | Predeterminado | Descripción |
|---|---|---|---|
callback | (...args) => void | - | Función a retrasar |
delay | number | - | Milisegundos a esperar |
options.leading | boolean | false | Dispara en el borde inicial |
| Devuelve | T & { cancel, flush } | - | Función retrasada con cancel y flush |
Modo inmediato (leading + trailing): Dispara en ambos bordes manteniendo un registro de si la llamada trailing también debe dispararse. Útil para botones de guardar donde quieres retroalimentación instantánea más un guardado final.
// Dispara en ambos bordes inicial y final
function useDebouncedCallback(callback, delay, { leading: true, trailing: true })Con maxWait: Garantiza que el callback se dispare al menos cada maxWait ms incluso si la entrada nunca se detiene. Combina debounce con un timer maxWait para limitar el retraso.
useDebouncedValue es genérico sobre T, por lo que el tipo devuelto coincide automáticamente con el tipo de entrada.useDebouncedCallback preserva los tipos de parámetro del callback original a través de Parameters<T>.as const no es necesario aquí ya que ambos hooks devuelven valores únicos u objetos.useDebouncedCallback siempre llama a la versión más reciente.setTimeout(fn, 0) aún se difiere al siguiente tick, lo que puede causar un destello visible. Solución: Usa un guard como if (delay <= 0) return callback(...) para casos de retraso cero.useEffect maneja esto; asegúrate de no omitirla.useDebouncedValue se inicializa con el valor sin procesar (sin setTimeout en el servidor), por lo que no hay desincronización de hidratación. No se necesita manejo especial.| Paquete | Nombre del Hook | Notas |
|---|---|---|
use-debounce (npm) | useDebounce, useDebouncedCallback | Más popular, soporta maxWait, leading/trailing |
usehooks-ts | useDebounce | Solo valores retrasados |
ahooks | useDebounce, useDebounceFn | Completo, parte de una colección grande |
lodash | _.debounce | No es un hook; envuelve en useMemo o useRef |
@uidotdev/usehooks | useDebounce | Minimal, solo valores |
De una aplicación SaaS Next.js 15 / React 19 en producción (SystemsArchitect.io).
// Ejemplo de producción: patrón debounce integrado con SWR
// Archivo: src/hooks/use-search.ts
import { useState, useEffect, useMemo } from 'react';
import useSWR from 'swr';
const fetcher = (url: string) => fetch(url).then((r) => r.json());
export function useSearch(query: string, platform: string) {
const [debouncedQuery, setDebouncedQuery] = useState(query);
// Debounce: solo actualiza la consulta después de 300ms de inactividad
useEffect(() => {
const timer = setTimeout(() => setDebouncedQuery(query), 300);
return () => clearTimeout(timer);
}, [query]);
const shouldFetch = debouncedQuery.length >= 3;
const apiUrl = shouldFetch
? `/api/search?q=${encodeURIComponent(debouncedQuery)}&platform=${platform}`
: null;
const { data, isLoading, isValidating } = useSWR(apiUrl, fetcher, {
keepPreviousData: true,
});
return useMemo(() => ({
results: data?.results ?? [],
isLoading: isLoading || (shouldFetch && isValidating),
}), [data, isLoading, shouldFetch, isValidating]);
}Lo que demuestra en producción:
setTimeout + clearTimeout limpieza en useEffect es el mecanismo debounce central. Cada vez que query cambia, el timeout anterior se borra y uno nuevo comienza. Solo cuando el usuario deja de escribir durante 300ms setDebouncedQuery se dispara.useDebouncedValue aplicado directamente en línea. En producción, se mantuvieron en línea en lugar de extraer a un hook separado porque el debounce está estrechamente acoplado con la lógica de fetch de SWR y la verificación de longitud mínima.return () => clearTimeout(timer) es crítica. Sin ella, una escritura rápida programaría múltiples timeouts, y consultas obsoletas se dispararían fuera de orden. La limpieza garantiza que solo el último timeout sobreviva.null de SWR (cuando shouldFetch es falso) previene el fetch completamente. No se realiza solicitud hasta que la consulta retrasada alcanza 3 caracteres.keepPreviousData: true previene un destello de resultados en blanco mientras se recupera la nueva consulta retrasada. Los resultados anteriores permanecen visibles hasta que llegan datos frescos.useDebouncedValue toma un valor reactivo y devuelve una copia retrasada que solo se actualiza después de que el retraso transcurra.useDebouncedCallback toma una función y devuelve una versión retrasada de esa función.useDebouncedValue cuando quieras retrasar un valor alimentando un useEffect. Usa useDebouncedCallback cuando quieras retrasar una acción como un clic de botón o guardar.setTimeout vería valores de cierre obsoletos del renderizado cuando se creó el timer.callbackRef.current) se actualiza en cada renderizado, por lo que el timer siempre llama a la versión más reciente del callback.Llama a cancel() en la función retrasada devuelta:
const debouncedSave = useDebouncedCallback(save, 1000);
// Más tarde:
debouncedSave.cancel();flush() invoca inmediatamente el callback pendiente con los últimos argumentos y reinicia el state.useEffect llama a clearTimeout, cancelando el timer pendiente.leading es true, el callback se dispara inmediatamente en la primera invocación.setTimeout(fn, 0) aún difiere la ejecución al siguiente tick, lo que puede causar un destello visible.if (delay <= 0) return callback(...).callbackRef.current en cada renderizado, el cierre de setTimeout haría referencia al callback del renderizado cuando se estableció el timer, no al más reciente.debouncedQuery.length >= 3 antes de construir la URL de la API.null como clave de SWR, lo que le dice a SWR que omita el fetch completamente.T, por lo que el tipo de devolución coincide automáticamente con el tipo de entrada.const debounced = useDebouncedValue("hello", 300);
// debounced se infiere como stringT extends (...args: any[]) => void genérico captura la firma de función completa.Parameters<T> extrae los tipos de argumento para que el envoltorio retrasado acepte los mismos argumentos que el callback original.Revisado por Chris St. John·Última actualización: 10 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥