Debouce | React
- Debouce | React
Debounce no React
Debounce é uma técnica usada para adiar a execução de uma função até que um determinado tempo passe sem novas chamadas. No React, ele é muito comum para evitar execuções repetidas em eventos de alta frequência, como digitação em inputs, scroll e resize.
Quando o debounce é útil
Use debounce quando você tem uma ação que não precisa acontecer a cada interação, e sim somente quando o usuário “terminar” por um momento.
Exemplos comuns:
- Busca em tempo real (search)
- Filtro de lista/tabela conforme digita
- Validação de formulário enquanto digita (email, CPF, etc.)
- Auto-save de texto (rascunho)
- Atualizações que chamam APIs
Diferença entre debounce e throttle
Debounce e throttle são parecidos, mas têm objetivos diferentes:
- Debounce: executa apenas depois que o usuário para
- Throttle: executa durante, mas limitando a frequência
Debounce é ideal quando você quer executar algo somente no “fim” da interação. Throttle é ideal quando você quer manter uma atualização constante, mas controlada.
Como funciona o debounce na prática
Quando você chama uma função com debounce:
- ela não executa imediatamente
- ela agenda uma execução para daqui “X ms”
- se for chamada novamente antes de “X ms”, o timer é reiniciado
- quando para de ser chamada, finalmente executa
Exemplo mental:
- delay = 700ms
- usuário digitando rapidamente:
r → re → rea → reac → react
A função só executa uma vez, no final:
✅ react
Implementação com setTimeout (sem biblioteca)
A forma mais simples é controlar manualmente com setTimeout.
import { useEffect, useState } from "react";
export function SearchInput() {
const [value, setValue] = useState("");
useEffect(() => {
const timer = setTimeout(() => {
console.log("Buscar:", value);
}, 700);
return () => clearTimeout(timer);
}, [value]);
return (
<input
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder="Buscar..."
/>
);
}
Detalhes importantes:
setTimeoutagenda a ação- o
return () => clearTimeout(timer)cancela o timer anterior - o efeito roda sempre que
valuemuda
Hook useDebounce (reutilizável)
Uma abordagem bem comum é criar um hook para você reutilizar em vários lugares.
import { useEffect, useState } from "react";
export function useDebounce<T>(value: T, delay = 700) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
Uso:
import { useEffect, useState } from "react";
import { useDebounce } from "./useDebounce";
export function SearchInput() {
const [search, setSearch] = useState("");
const debouncedSearch = useDebounce(search, 700);
useEffect(() => {
console.log("Buscar:", debouncedSearch);
}, [debouncedSearch]);
return (
<input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Buscar..."
/>
);
}
Essa abordagem facilita:
- separar “valor atual” de “valor estabilizado”
- evitar erros de execução duplicada
- manter o código mais limpo
Debounce com lodash.debounce
Uma biblioteca popular é lodash.debounce.
Instalação:
npm i lodash.debounce
Exemplo:
import debounce from "lodash.debounce";
import { useEffect, useMemo, useState } from "react";
export function SearchInput() {
const [value, setValue] = useState("");
const debouncedSearch = useMemo(() => {
return debounce((text: string) => {
console.log("Buscar:", text);
}, 700);
}, []);
useEffect(() => {
return () => {
debouncedSearch.cancel();
};
}, [debouncedSearch]);
return (
<input
value={value}
onChange={(e) => {
setValue(e.target.value);
debouncedSearch(e.target.value);
}}
placeholder="Buscar..."
/>
);
}
Pontos importantes:
useMemoimpede recriar o debounce a cada renderdebouncedSearch.cancel()evita chamadas depois que o componente desmonta- passar o valor por parâmetro reduz chance de usar state “atrasado”
Por que useMemo é usado junto com debounce
useMemo é usado para que a função debounced seja estável entre renders.
Se você fizer isso sem memoização:
const debouncedSearch = debounce(fn, 700);
Você recria a função em todo render, o que pode causar:
- perda do timer anterior
- comportamento inconsistente
- múltiplas funções “penduradas”
- bugs difíceis de rastrear
Com useMemo, você cria uma única instância:
const debouncedSearch = useMemo(() => debounce(fn, 700), [fn]);
Cancelamento do debounce e limpeza correta
Debounce quase sempre deve ser cancelado em dois casos:
- quando o componente desmonta
- quando você vai disparar a ação manualmente (ex: Enter)
No lodash:
debounced.cancel()cancela execução pendentedebounced.flush()executa imediatamente o que estava pendente
Exemplo com limpeza:
useEffect(() => {
return () => {
debouncedSearch.cancel();
};
}, [debouncedSearch]);
Isso evita situações como:
- busca executando depois que a tela mudou
- warning de state update em componente desmontado
Enter + debounce: cuidado com chamadas duplicadas
Um erro comum é:
- digitar (debounce agenda busca)
- apertar Enter (busca manual executa)
- depois de 700ms o debounce executa de novo
Isso gera duplicidade de requisições.
Forma correta:
const handleEnter = () => {
debouncedSearch.cancel();
onSearch(value);
};
Assim você garante:
- Enter executa imediatamente
- nenhuma chamada pendente acontece depois
Debounce com componente controlado vs não-controlado
Componente controlado
O valor vem do props.value e o pai controla o estado.
✅ Mais previsível, ideal para formulários grandes.
Risco:
- re-render frequente se o pai não estiver bem otimizado
onSearchpode mudar e recriar o debounce se não estiver memoizado
Componente não-controlado
O valor fica dentro do componente (useState) e ele controla o input.
✅ Mais simples em componentes menores.
Risco:
- sincronização com valor externo pode ser mais trabalhosa
Problemas comuns com debounce no React
Debounce executando com valor antigo
Isso pode acontecer quando a função captura valores antigos (closure).
Boa prática:
- passar o texto como argumento para o debounce
- evitar depender de state dentro da função debounced
✅ Exemplo:
debouncedSearch(e.target.value);
Debounce recriando várias vezes
Se o useMemo depende de funções que mudam em todo render, o debounce também muda.
✅ Soluções:
- usar
useCallbackno componente pai - evitar criar
onSearchinline
Demora percebida pelo usuário
Debounce alto demais pode gerar sensação de “travamento”.
Ajustes comuns:
- 200ms a 400ms para UX mais rápida
- 500ms a 900ms para buscas em API
- 1000ms+ para auto-save
Cancelamento inexistente pode gerar warning
Se o debounce roda após desmontar o componente, você pode ver avisos do React e comportamentos estranhos.
✅ Sempre cancele com cleanup.
Boas práticas ao usar debounce em buscas
- ✅ mostrar loader enquanto busca
- ✅ mostrar mensagem de “nenhum resultado”
- ✅ evitar busca vazia (opcional)
- ✅ cancelar requisições anteriores no backend (quando possível)
- ✅ tratar erro e fallback
Checklist rápido
useMemopara manter debounce estávelcancel()no cleanup- cuidado com Enter executando busca duplicada
- passar o valor por argumento para evitar closure velha
- escolher delay adequado para a experiência