Busque em todas as páginas da documentação
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Dois hooks complementares: useDebouncedValue atrasa um valor reativo; useDebouncedCallback atrasa uma chamada de função.
import { useState, useEffect, useRef, useCallback, useMemo } from "react";
/**
* useDebouncedValue
* Retorna uma cópia com debounce do `value` que só atualiza
* após `delay` ms de inatividade.
*/
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
* Retorna uma versão estável e com debounce do `callback`.
* Suporta invocação na borda inicial (leading edge) via `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);
// Sempre mantém o callback mais recente
useEffect(() => {
callbackRef.current = callback;
}, [callback]);
// Limpeza ao 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;
}Quando usar isso: Você precisa evitar sobrecarregar uma API a cada pressionamento de tecla (pesquisa enquanto digita) ou deseja agrupar eventos rápidos como redimensionamento ou rolagem em uma única atualização.
"use client";
import { useState } from "react";
function SearchInput() {
const [query, setQuery] = useState("");
const debouncedQuery = useDebouncedValue(query, 300);
// Só dispara a requisição de rede quando debouncedQuery muda
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="Pesquisar..."
/>
);
}
function SaveButton() {
const debouncedSave = useDebouncedCallback(
(content: string) => {
fetch("/api/save", {
method: "POST",
body: JSON.stringify({ content }),
});
},
1000,
{ leading: true }
);
return (
<button onClick={() => debouncedSave("conteúdo rascunhado")}>
Salvar (com debounce)
</button>
);
}O que isso demonstra:
useDebouncedValue atrasa a consulta de pesquisa para que a API seja chamada somente após o usuário parar de digitar por 300 ms.useDebouncedCallback com leading: true dispara imediatamente no primeiro clique, e depois ignora cliques rápidos por 1 segundo.setTimeout toda vez que value muda. A função de limpeza em useEffect cancela o timer anterior, então apenas a última atualização dentro de delay ms realmente chega ao estado.cancel() cancela qualquer invocação pendente. flush() invoca o callback pendente imediatamente e redefine o estado.| Parâmetro | Tipo | Padrão | Descrição |
|---|---|---|---|
value | T | - | O valor a ser com debounce |
delay | number | - | Milissegundos para esperar |
| Retorna | T | - | O valor com debounce |
| Parâmetro | Tipo | Padrão | Descrição |
|---|---|---|---|
callback | (...args) => void | - | Função a ser com debounce |
delay | number | - | Milissegundos para esperar |
options.leading | boolean | false | Dispara na borda inicial (leading edge) |
| Retorna | T & { cancel, flush } | - | Função com debounce com cancel e flush |
Modo imediato (leading + trailing): Dispara em ambas as bordas rastreando se a chamada trailing também deve disparar. Útil para botões de salvar onde você quer feedback instantâneo mais um salvamento final.
// Dispara nas bordas inicial e final (leading e trailing)
function useDebouncedCallback(callback, delay, { leading: true, trailing: true })Com maxWait: Garante que o callback dispare pelo menos a cada maxWait ms, mesmo que a entrada nunca pare. Combine debounce com um timer maxWait para limitar o atraso.
useDebouncedValue é genérico sobre T, então o tipo de retorno corresponde automaticamente ao tipo de entrada.useDebouncedCallback preserva os tipos de parâmetro do callback original via Parameters<T>.as const não é necessário aqui, pois ambos os hooks retornam valores ou objetos únicos.useDebouncedCallback sempre chama a versão mais recente.setTimeout(fn, 0) ainda adia para o próximo tick, o que pode causar um flash visível. Correção: Use uma guarda como if (delay <= 0) return callback(...) para casos de atraso zero.useEffect cuida disso; certifique-se de não ignorá-la.useDebouncedValue inicializa com o valor bruto (sem setTimeout no servidor), então não há incompatibilidade de hidratação. Nenhuma manipulação especial é necessária.| Pacote | Nome do Hook | Notas |
|---|---|---|
use-debounce (npm) | useDebounce, useDebouncedCallback | Mais popular, suporta maxWait, leading/trailing |
usehooks-ts | useDebounce | Debounce apenas para valor |
ahooks | useDebounce, useDebounceFn | Completo, parte de uma grande coleção |
lodash | _.debounce | Não é um hook; envolva em useMemo ou useRef |
@uidotdev/usehooks | useDebounce | Mínimo, apenas para valor |
De uma aplicação SaaS de produção em Next.js 15 / React 19 (SystemsArchitect.io).
// Exemplo de produção: Padrão de debounce integrado com SWR
// Arquivo: 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: atualiza a consulta apenas após 300ms de inatividade
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]);
}O que isso demonstra em produção:
setTimeout + limpeza clearTimeout em useEffect é o mecanismo central de debounce. Cada vez que query muda, o timeout anterior é cancelado e um novo começa. Somente quando o usuário para de digitar por 300ms, setDebouncedQuery é acionado.useDebouncedValue aplicado diretamente inline. Em produção, foi mantido inline em vez de extraído para um hook separado porque o debounce está intimamente ligado à lógica de fetch do SWR e à verificação de comprimento mínimo.return () => clearTimeout(timer) é crítica. Sem ela, digitação rápida agendaria múltiplos timeouts, e consultas desatualizadas seriam disparadas fora de ordem. A limpeza garante que apenas o último timeout sobreviva.null do SWR (quando shouldFetch é falso) impede o fetch completamente. Nenhuma requisição é feita até que a consulta com debounce atinja 3 caracteres.keepPreviousData: true impede um flash de resultados em branco enquanto a nova consulta com debounce está sendo buscada. Os resultados anteriores permanecem visíveis até que novos dados cheguem.useDebouncedValue recebe um valor reativo e retorna uma cópia atrasada que só atualiza após o atraso expirar.useDebouncedCallback recebe uma função e retorna uma versão com debounce dessa função.useDebouncedValue quando quiser atrasar um valor que alimenta um useEffect. Use useDebouncedCallback quando quiser fazer debounce de uma ação como um clique de botão ou salvar.setTimeout veria valores de closure desatualizados da renderização quando o timer foi criado.callbackRef.current) é atualizada a cada renderização, então o timer sempre chama a versão mais recente do callback.Chame cancel() na função debounced retornada:
const debouncedSave = useDebouncedCallback(save, 1000);
// Mais tarde:
debouncedSave.cancel();flush() invoca imediatamente o callback pendente com os últimos argumentos e redefine o estado.useEffect chama clearTimeout, cancelando o timer pendente.leading é true, o callback dispara imediatamente na primeira invocação.setTimeout(fn, 0) ainda adia a execução para o próximo tick, o que pode causar um flash visível.if (delay <= 0) return callback(...).callbackRef.current a cada renderização, a closure setTimeout referenciaria o callback da renderização quando o timer foi definido, não o mais recente.debouncedQuery.length >= 3 antes de construir a URL da API.null como a chave do SWR, o que instrui o SWR a pular o fetch completamente.T, então o tipo de retorno corresponde automaticamente ao tipo de entrada.const debounced = useDebouncedValue("olá", 300);
// debounced é inferido como stringT extends (...args: any[]) => void captura a assinatura completa da função.Parameters<T> extrai os tipos de argumento para que o wrapper debounced aceite os mesmos argumentos que o callback original.Revisado por Chris St. John·Última atualização: 10 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥