Skip to content

useLoop ​

Description ​

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.


Signature ​

ts
// 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 when manual: 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 single run() function.


Options (UseLoopOptions) ​

ts
{
  /** 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, call resume() inside the callback or run() from outside.
  • disabled — pauses the loop; switching it back to false resumes the loop.
  • resetElapsedOnResume:
    • false — continue with the remaining time of the current interval;
    • true — next interval starts from zero with full duration.

Behavior ​

  1. Each tick schedules the next one according to the options (manual, disabled).
  2. The callback receives actualTime — the real duration of the last interval.
  3. Pausing (disabled: true) stops the loop. Resuming continues either with the leftover time or from zero — depending on resetElapsedOnResume.
  4. Changing durationMs is applied predictably: it either respects the current progress (if configured so) or takes effect on the next tick.
  5. On unmount, the timer is properly cleared.

Examples ​

1) Basic loop, manual start ​

tsx
import { useLoop } from '@webeach/react-hooks';

export function Example() {
  const [run] = useLoop(({ actualTime }) => {
    console.log('tick ~', actualTime, 'ms');
  }, 1000);

  return <button onClick={run}>Start</button>;
}

2) Autostart and automatic rescheduling ​

tsx
import { useLoop } from '@webeach/react-hooks';

export function Example() {
  const [run] = useLoop(
    ({ actualTime }) => {
      // runs roughly every ~500ms
    },
    {
      durationMs: 500,
      autorun: true,
    },
  );

  return <button onClick={run}>Restart</button>;
}

3) Manual mode: one tick at a time ​

tsx
import { useLoop } from '@webeach/react-hooks';

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>;
}

4) Pause/resume from the outside ​

tsx
import { useState } from 'react';
import { useLoop } from '@webeach/react-hooks';

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>
    </>
  );
}

5) Changing duration without resetting leftover time ​

tsx
import { useState } from 'react';
import { useLoop } from '@webeach/react-hooks';

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>
    </>
  );
}

When to Use ​

  • 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).

When Not to Use ​

  • You need a single run after N ms — use useTimeout or useTimeoutExtended instead.
  • You need frame‑based ticks — prefer useFrame or useFrameExtended.
  • You need to catch up missed ticks in batches — useLoop schedules one tick at a time and does not perform catch‑up.

Common Mistakes ​

  • In manual: true the loop will not continue by itself — you must call resume() in the callback or run() from outside.
  • autorun with disabled: true will not start anything until disabled is turned off.
  • With resetElapsedOnResume: true, changing durationMs does not affect the current timer — only the next one.

Types ​

Exported types

  • UseLoopCallback(options) — { actualTime: number; resume: () => void }.
  • UseLoopOptions — see options above.
  • UseLoopReturn — externally you get run() (tuple/object depending on how you destructure).

See also ​