Busca en todas las páginas de la documentación
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Basado en el patrón useInterval de Dan Abramov, extendido con pausa/reanudación y retraso dinámico.
import { useEffect, useRef, useCallback, useState } from "react";
/**
* useInterval
* Un hook setInterval declarativo. Limpia automáticamente al desmontar.
* Pasa `null` como delay para pausar.
*/
function useInterval(
callback: () => void,
delay: number | null
): void {
const savedCallback = useRef(callback);
// Recuerda el callback más reciente sin reiniciar el intervalo
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
if (delay === null) return;
const id = setInterval(() => savedCallback.current(), delay);
return () => clearInterval(id);
}, [delay]);
}
/**
* useCountdown
* Temporizador de cuenta regresiva construido sobre useInterval.
*/
interface UseCountdownOptions {
/** Valor inicial en segundos */
seconds: number;
/** Intervalo en ms. Predeterminado: 1000 */
interval?: number;
/** Comenzar inmediatamente. Predeterminado: false */
autoStart?: boolean;
/** Llamado cuando la cuenta regresiva llega a cero */
onComplete?: () => void;
}
interface UseCountdownReturn {
/** Segundos restantes */
remaining: number;
/** Si la cuenta regresiva está en ejecución */
isRunning: boolean;
/** Inicia o reanuda la cuenta regresiva */
start: () => void;
/** Pausa la cuenta regresiva */
pause: () => void;
/** Reinicia al valor inicial y detiene */
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 };
}Cuándo usarlo: Necesitas un temporizador, bucle de polling, auto-actualización o cuenta regresiva que interactúe con el estado de React sin sufrir errores de cierre obsoleto.
"use client";
// Contador incrementado automáticamente
function TickCounter() {
const [count, setCount] = useState(0);
const [delay, setDelay] = useState<number | null>(1000);
useInterval(() => {
setCount((c) => c + 1);
}, delay);
return (
<div>
<p>Contador: {count}</p>
<button onClick={() => setDelay(delay ? null : 1000)}>
{delay ? "Pausar" : "Reanudar"}
</button>
<button onClick={() => setDelay(500)}>Acelerar (500ms)</button>
<button onClick={() => setDelay(2000)}>Desacelerar (2s)</button>
</div>
);
}
// Temporizador de cuenta regresiva
function Timer() {
const { remaining, isRunning, start, pause, reset } = useCountdown({
seconds: 60,
onComplete: () => alert("¡Se acabó el tiempo!"),
});
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}>Iniciar</button>
) : (
<button onClick={pause}>Pausar</button>
)}
<button onClick={reset}>Reiniciar</button>
</div>
</div>
);
}
// Polling de API
function PollingStatus() {
const [status, setStatus] = useState("desconocido");
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); // Detener polling cuando está listo
}
} catch {
setStatus("error");
}
},
polling ? 5000 : null
);
return (
<div>
<p>Estado: {status}</p>
<p>{polling ? "Haciendo polling cada 5s..." : "Polling detenido"}</p>
</div>
);
}Lo que esto demuestra:
null) o reanudar en tiempo de ejecuciónsetInterval simple captura el callback en el momento de la creación. Si el callback hace referencia al estado, ve valores obsoletos. El patrón de Dan Abramov resuelve esto almacenando el callback más reciente en un ref.savedCallback.current se actualiza en cada renderizado a través de useEffect, por lo que el intervalo siempre llama a la versión más reciente del callback.delay, por lo que solo se reinicia cuando cambia el delay, no cuando cambia el callback. Esto preserva el timing.null como el delay hace que el efecto omita setInterval completamente, pausando efectivamente el temporizador sin perder el estado.clearInterval, por lo que cambiar el delay o desmontar siempre limpia el intervalo anterior.| Parámetro | Tipo | Predeterminado | Descripción |
|---|---|---|---|
callback | () => void | - | Función a llamar en cada tick |
delay | number o null | - | Intervalo en ms, o null para pausar |
| Opción | Tipo | Predeterminado | Descripción |
|---|---|---|---|
seconds | number | - | Valor inicial de la cuenta regresiva |
interval | number | 1000 | Intervalo de tick en ms |
autoStart | boolean | false | Comenzar a contar inmediatamente |
onComplete | () => void | - | Llamado cuando la cuenta regresiva llega a cero |
| Retorno | Tipo | Descripción |
|---|---|---|
remaining | number | Segundos restantes |
isRunning | boolean | Si la cuenta regresiva está activa |
start | () => void | Iniciar o reanudar |
pause | () => void | Pausar |
reset | () => void | Reiniciar a segundos iniciales y detener |
useTimeout: El mismo patrón 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]);
}Temporizador preciso: setInterval puede desviarse con el tiempo. Para temporizadores precisos, calcula el tiempo transcurrido desde una marca de tiempo inicial:
const startRef = useRef(Date.now());
useInterval(() => {
const elapsed = Math.floor((Date.now() - startRef.current) / 1000);
setRemaining(Math.max(0, totalSeconds - elapsed));
}, 100); // Verificar frecuentemente, calcular desde el reloj de pared() => void ya que los callbacks de intervalo típicamente no devuelven valores.delay acepta number | null donde null es la señal de pausa.setInterval no espera callbacks asincronos. Si el callback toma más tiempo que el intervalo, las llamadas se superponen. Solución: Usa un flag para omitir ticks mientras una llamada anterior está pendiente, o usa setTimeout recursivo en su lugar.setInterval garantiza un retraso mínimo, no un timing exacto. Con el tiempo, la desviación se acumula. Solución: Usa la variación de reloj de pared mostrada arriba para precisión.requestAnimationFrame para actualizaciones visuales, o acepta el throttling para polling.clearInterval no se llama, el callback continúa disparándose. Solución: La limpieza del efecto maneja esto; nunca uses setInterval fuera de este patrón hook en React.| Paquete | Nombre del hook | Notas |
|---|---|---|
usehooks-ts | useInterval, useCountdown | Popular, bien documentado |
ahooks | useInterval, useCountDown | Completo con formato |
@uidotdev/usehooks | useInterval | Mínimo, mismo patrón |
react-use | useInterval | Soporta primer tick inmediato |
react-timer-hook | useTimer, useStopwatch | Componentes temporizador especializados |
setInterval simple captura el callback en el momento de la creación, por lo que ve valores de estado obsoletos.useInterval almacena el callback en un ref (savedCallback.current) y lo actualiza en cada renderizado, por lo que el intervalo siempre llama a la versión más reciente.Pasa null como el delay para pausar y un número para reanudar:
const [delay, setDelay] = useState<number | null>(1000);
useInterval(() => tick(), delay);
// Pausar: setDelay(null)
// Reanudar: setDelay(1000)remaining <= 1.isRunning a false (que pasa null como el delay) y llama a onComplete.setInterval no espera callbacks asincronos, por lo que las llamadas se superpondrán.setTimeout recursivo en su lugar.setInterval garantiza un retraso mínimo, no un timing exacto. Con el tiempo, la desviación se acumula.Date.now() en lugar de contar ticks.requestAnimationFrame. Para polling, acepta el throttling.Reemplaza setInterval con setTimeout y clearInterval con 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 y UseCountdownReturn) tanto para las opciones como para el valor de retorno.null es una señal explícita de "pausado" que es fácil de verificar: if (delay === null) return.undefined podría ser ambiguo con un argumento faltante u olvidado.delay cambia, el efecto limpia el intervalo anterior y crea uno nuevo con el delay actualizado.Revisado por Chris St. John·Última actualización: 19 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥