useLoop is a hook for periodic execution built on one‑shot setTimeouts. It supports pause/resume, manual and automatic modes, uses performance.now() for accurate timing, and can carry over or reset the remaining time when resuming.
If you need a single‑shot timer with extended controls — see useTimeoutExtended. For a simple one‑off run — use useTimeout.
// Overload 1: fixed duration
function useLoop(
callback: UseLoopCallback,
durationMs: number,
): UseLoopReturn;
// Overload 2: full configuration
function useLoop(
callback: UseLoopCallback,
options: UseLoopOptions,
): UseLoopReturn;Parameters
callback— called on each tick. Receives{ actualTime, resume }:actualTime— the actual elapsed time (ms) for the current interval;resume()— continue the loop (useful whenmanual: true).
durationMs— interval duration in ms for the short overload.options— full configuration (see below).
Returns
- A control function
run()— start or resume the loop with the current options.
The return supports tuple/object forms (via
useDemandStructure), but practically you use a singlerun()function.
{
/** autostart on mount (default false) */
autorun?: boolean;
/** external pause/resume (default false) */
disabled?: boolean;
/** duration of a single interval (ms) */
durationMs: number;
/** manual mode: one tick then wait (default false) */
manual?: boolean;
/** on resume/when duration changes — reset leftover time? (default false) */
resetElapsedOnResume?: boolean;
}manual: false— after each tick the hook auto‑schedules the next one.manual: true— runs one tick and then pauses. To continue, callresume()inside thecallbackorrun()from outside.disabled— pauses the loop; switching it back tofalseresumes the loop.resetElapsedOnResume:false— continue with the remaining time of the current interval;true— next interval starts from zero with full duration.
- Each tick schedules the next one according to the options (
manual,disabled). - The
callbackreceivesactualTime— the real duration of the last interval. - Pausing (
disabled: true) stops the loop. Resuming continues either with the leftover time or from zero — depending onresetElapsedOnResume. - Changing
durationMsis applied predictably: it either respects the current progress (if configured so) or takes effect on the next tick. - On unmount, the timer is properly cleared.
import { useLoop } from '@webeach/react-hooks/useLoop';
export function Example() {
const [run] = useLoop(({ actualTime }) => {
console.log('tick ~', actualTime, 'ms');
}, 1000);
return <button onClick={run}>Start</button>;
}import { useLoop } from '@webeach/react-hooks/useLoop';
export function Example() {
const [run] = useLoop(({ actualTime }) => {
// runs roughly every ~500ms
}, {
durationMs: 500,
autorun: true,
});
return <button onClick={run}>Restart</button>;
}import { useLoop } from '@webeach/react-hooks/useLoop';
export function Example() {
const [run] = useLoop(({ actualTime, resume }) => {
doWork();
if (shouldContinue()) {
resume(); // continue the next tick right from the callback
}
}, {
durationMs: 800,
manual: true,
});
return <button onClick={run}>Tick</button>;
}import { useState } from 'react';
import { useLoop } from '@webeach/react-hooks/useLoop';
export function Example() {
const [paused, setPaused] = useState(false);
const [run] = useLoop(() => {
// ...
}, {
durationMs: 1000,
autorun: true,
disabled: paused,
});
return (
<>
<button onClick={() => setPaused(true)}>Pause</button>
<button onClick={() => setPaused(false)}>Resume</button>
<button onClick={run}>Restart</button>
</>
);
}import { useState } from 'react';
import { useLoop } from '@webeach/react-hooks/useLoop';
export function Example() {
const [ms, setMs] = useState(1000);
const run = useLoop(() => {}, { durationMs: ms, autorun: true, resetElapsedOnResume: false });
return (
<>
<button onClick={() => setMs(2000)}>Make 2s</button>
<button onClick={run}>Restart</button>
</>
);
}- You need periodic logic with pause/resume.
- You want to account for drift (get the actual tick time via
actualTime). - You need a manual mode (continue from inside the
callback).
- You need a single run after N ms — use
useTimeoutoruseTimeoutExtendedinstead. - You need frame‑based ticks — prefer
useFrameoruseFrameExtended. - You need to catch up missed ticks in batches —
useLoopschedules one tick at a time and does not perform catch‑up.
- In
manual: truethe loop will not continue by itself — you must callresume()in thecallbackorrun()from outside. autorunwithdisabled: truewill not start anything untildisabledis turned off.- With
resetElapsedOnResume: true, changingdurationMsdoes not affect the current timer — only the next one.
Exported types
UseLoopCallback(options)—{ actualTime: number; resume: () => void }.UseLoopOptions— see options above.UseLoopReturn— externally you getrun()(tuple/object depending on how you destructure).