Busque em todas as páginas da documentação
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Baseado no padrão useInterval de Dan Abramov, estendido com pausa/retomada e atraso dinâmico.
import { useEffect, useRef, useCallback, useState } from "react";
/**
* useInterval
* A declarative setInterval hook. Automatically cleans up on unmount.
* Pass `null` as delay to pause.
*/
function useInterval(
callback: () => void,
delay: number | null
): void {
const savedCallback = useRef(callback);
// Remember the latest callback without restarting the interval
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
if (delay === null) return;
const id = setInterval(() => savedCallback.current(), delay);
return () => clearInterval(id);
}, [delay]);
}
/**
* useCountdown
* Countdown timer built on useInterval.
*/
interface UseCountdownOptions {
/** Starting value in seconds */
seconds: number;
/** Interval in ms. Default: 1000 */
interval?: number;
/** Start immediately. Default: false */
autoStart?: boolean;
/** Called when countdown reaches zero */
onComplete?: () => void;
}
interface UseCountdownReturn {
/** Remaining seconds */
remaining: number;
/** Whether the countdown is running */
isRunning: boolean;
/** Start or resume the countdown */
start: () => void;
/** Pause the countdown */
pause: () => void;
/** Reset to the initial value and stop */
reset: () => void;
}
function useCountdown(options: UseCountdownOptions): UseCountdownReturn {
const {
seconds,
interval = 1000,
autoStart = false,
onComplete,
} = options;
const [remaining, setRemaining] = useState(seconds);
const [isRunning, setIsRunning] = useState(autoStart);
const onCompleteRef = useRef(onComplete);
useEffect(() => {
onCompleteRef.current = onComplete;
}, [onComplete]);
useInterval(
() => {
setRemaining((prev) => {
if (prev <= 1) {
setIsRunning(false);
onCompleteRef.current?.();
return 0;
}
return prev - 1;
});
},
isRunning ? interval : null
);
const start = useCallback(() => {
if (remaining > 0) setIsRunning(true);
}, [remaining]);
const pause = useCallback(() => setIsRunning(false), []);
const reset = useCallback(() => {
setIsRunning(false);
setRemaining(seconds);
}, [seconds]);
return { remaining, isRunning, start, pause, reset };
}Quando usar isso: Você precisa de um timer, loop de polling, auto-refresh ou contagem regressiva que interaja com o estado do React sem sofrer com bugs de closure obsoleto.
"use client";
// Auto-incrementing counter
function TickCounter() {
const [count, setCount] = useState(0);
const [delay, setDelay] = useState<number | null>(1000);
useInterval(() => {
setCount((c) => c + 1);
}, delay);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setDelay(delay ? null : 1000)}>
{delay ? "Pause" : "Resume"}
</button>
<button onClick={() => setDelay(500)}>Speed Up (500ms)</button>
<button onClick={() => setDelay(2000)}>Slow Down (2s)</button>
</div>
);
}
// Countdown timer
function Timer() {
const { remaining, isRunning, start, pause, reset } = useCountdown({
seconds: 60,
onComplete: () => alert("Time is up!"),
});
const minutes = Math.floor(remaining / 60);
const secs = remaining % 60;
return (
<div>
<p style={{ fontSize: 48, fontFamily: "monospace" }}>
{String(minutes).padStart(2, "0")}:{String(secs).padStart(2, "0")}
</p>
<div style={{ display: "flex", gap: 8 }}>
{!isRunning ? (
<button onClick={start}>Start</button>
) : (
<button onClick={pause}>Pause</button>
)}
<button onClick={reset}>Reset</button>
</div>
</div>
);
}
// API polling
function PollingStatus() {
const [status, setStatus] = useState("unknown");
const [polling, setPolling] = useState(true);
useInterval(
async () => {
try {
const res = await fetch("/api/status");
const data = await res.json();
setStatus(data.status);
if (data.status === "complete") {
setPolling(false); // Stop polling when done
}
} catch {
setStatus("error");
}
},
polling ? 5000 : null
);
return (
<div>
<p>Status: {status}</p>
<p>{polling ? "Polling every 5s..." : "Polling stopped"}</p>
</div>
);
}O que isso demonstra:
null) ou retomado em tempo de execução.setInterval simples captura o callback no momento da criação. Se o callback referencia o estado, ele vê valores obsoletos. O padrão de Dan Abramov resolve isso armazenando o último callback em uma ref.savedCallback.current é atualizado a cada renderização via useEffect, então o intervalo sempre chama a versão mais recente do callback.delay, então ele só reinicia quando o delay muda, não quando o callback muda. Isso preserva o tempo.null como delay faz com que o efeito pule setInterval inteiramente, efetivamente pausando o timer sem perder o estado.clearInterval, então mudar o delay ou desmontar sempre limpa o intervalo anterior.| Parâmetro | Tipo | Padrão | Descrição |
|---|---|---|---|
callback | () => void | - | Função a ser chamada a cada tick |
delay | number ou null | - | Intervalo em ms, ou null para pausar |
| Opção | Tipo | Padrão | Descrição |
|---|---|---|---|
seconds | number | - | Valor inicial da contagem regressiva |
interval | number | 1000 | Intervalo de tick em ms |
autoStart | boolean | false | Inicia a contagem imediatamente |
onComplete | () => void | - | Chamado quando a contagem regressiva atinge zero |
| Retorno | Tipo | Descrição |
|---|---|---|
remaining | number | Segundos restantes |
isRunning | boolean | Se a contagem regressiva está ativa |
start | () => void | Iniciar ou retomar |
pause | () => void | Pausar |
reset | () => void | Resetar para os segundos iniciais e parar |
useTimeout: O mesmo padrão funciona para setTimeout:
function useTimeout(callback: () => void, delay: number | null): void {
const savedCallback = useRef(callback);
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
if (delay === null) return;
const id = setTimeout(() => savedCallback.current(), delay);
return () => clearTimeout(id);
}, [delay]);
}Timer preciso: setInterval pode derivar com o tempo. Para timers precisos, calcule o tempo decorrido a partir de um timestamp de início:
const startRef = useRef(Date.now());
useInterval(() => {
const elapsed = Math.floor((Date.now() - startRef.current) / 1000);
setRemaining(Math.max(0, totalSeconds - elapsed));
}, 100); // Check frequently, calculate from wall clock() => void, pois os callbacks de intervalo normalmente não retornam valores.delay aceita number | null, onde null é o sinal de pausa.setInterval não aguarda callbacks assíncronos. Se o callback demorar mais que o intervalo, as chamadas se sobrepõem. Correção: Use uma flag para pular ticks enquanto uma chamada anterior está pendente, ou use setTimeout recursivo em vez disso.setInterval garante um atraso mínimo, não um tempo exato. Ao longo dos minutos, a deriva se acumula. Correção: Use a variação do relógio de parede mostrada acima para precisão.requestAnimationFrame para atualizações visuais, ou aceite a limitação para polling.clearInterval não for chamado, o callback continuará disparando. Correção: A limpeza do efeito cuida disso; nunca use setInterval fora deste padrão de hook no React.| Pacote | Nome do Hook | Notas |
|---|---|---|
usehooks-ts | useInterval, useCountdown | Popular, bem documentado |
ahooks | useInterval, useCountDown | Completo com formatação |
@uidotdev/usehooks | useInterval | Mínimo, mesmo padrão |
react-use | useInterval | Suporta o primeiro tick imediato |
react-timer-hook | useTimer, useStopwatch | Componentes de timer especializados |
setInterval simples captura o callback no momento da criação, então ele vê valores de estado obsoletos.useInterval armazena o callback em uma ref (savedCallback.current) e o atualiza a cada renderização, então o intervalo sempre chama a versão mais recente.Passe null como delay para pausar e um número para retomar:
const [delay, setDelay] = useState<number | null>(1000);
useInterval(() => tick(), delay);
// Pause: setDelay(null)
// Resume: setDelay(1000)remaining <= 1.isRunning como false (o que passa null como delay) e chama onComplete.setInterval não aguarda callbacks assíncronos, então as chamadas se sobrepõem.setTimeout recursivo em vez disso.setInterval garante um atraso mínimo, não um tempo exato. Ao longo dos minutos, a deriva se acumula.Date.now() em vez de contar ticks.requestAnimationFrame. Para polling, aceite a limitação.Substitua setInterval por setTimeout e clearInterval por clearTimeout:
function useTimeout(callback: () => void, delay: number | null) {
const savedCallback = useRef(callback);
useEffect(() => { savedCallback.current = callback; }, [callback]);
useEffect(() => {
if (delay === null) return;
const id = setTimeout(() => savedCallback.current(), delay);
return () => clearTimeout(id);
}, [delay]);
}UseCountdownOptions e UseCountdownReturn) tanto para as opções quanto para o valor de retorno.null é um sinal explícito de "pausado" que é fácil de verificar: if (delay === null) return.undefined poderia ser ambíguo com um argumento ausente ou esquecido.delay muda, o efeito limpa o intervalo antigo e cria um novo com o delay atualizado.Revisado por Chris St. John·Última atualização: 19 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥