Skip to main content

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:

rrereareacreact

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:

  • setTimeout agenda a ação
  • o return () => clearTimeout(timer) cancela o timer anterior
  • o efeito roda sempre que value muda

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:

  • useMemo impede recriar o debounce a cada render
  • debouncedSearch.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:

  1. quando o componente desmonta
  2. quando você vai disparar a ação manualmente (ex: Enter)

No lodash:

  • debounced.cancel() cancela execução pendente
  • debounced.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
  • onSearch pode 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 useCallback no componente pai
  • evitar criar onSearch inline

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

  • useMemo para manter debounce estável
  • cancel() no cleanup
  • cuidado com Enter executando busca duplicada
  • passar o valor por argumento para evitar closure velha
  • escolher delay adequado para a experiência