Skip to content
usefy

usePolling

Timing & async

Poll an async function on an interval — non-overlapping ticks, pause/resume, an enabled gate, and exponential backoff.

Install

$ npm install @usefy/use-polling

Quick start

usePolling.tsx
import { usePolling } from "@usefy/use-polling";

function LiveStatus() {
  const { data, status, isPolling, pause, resume } = usePolling(
    async (signal: AbortSignal) => {
      const res = await fetch("/api/status", { signal });
      return (await res.json()) as { online: number };
    },
    { interval: 5000, immediate: true, backoff: { factor: 2, maxInterval: 30_000 } },
  );

  return (
    <div>
      <p>{data ? `${data.online} online` : "…"} ({status})</p>
      <button onClick={isPolling ? pause : resume}>
        {isPolling ? "Pause" : "Resume"}
      </button>
    </div>
  );
}

API reference

const {
  data, error, status, isLoading,
  isPolling, pause, resume, start, stop,
} = usePolling<T, Args, E>(fn, options?);

Parameters

ParameterTypeDescription
fn(signal: AbortSignal, ...args: Args) => Promise<T>The async function to poll. Receives an AbortSignal first, then options.args. Wire the signal into fetch(url, { signal }). Read through a ref — an inline function is fine and never goes stale.
optionsUsePollingOptions<T, Args, E>Optional. See below.

Options — UsePollingOptions<T, Args, E>

OptionTypeDefaultDescription
intervalnumber1000Base delay (ms) between one poll settling and the next starting. Applies from the next tick — never restarts the loop.
immediatebooleantruePoll immediately when the loop starts/resumes, vs. waiting one interval.
enabledbooleantrueDeclarative master gate. false → never polls; flipping to true (re)starts the loop. While false, resume() cannot start polling.
backoffboolean | { factor?, maxInterval? } | (failures, base) => numberfalseGrow the delay on consecutive failures, reset on success. true → exponential factor: 2; object → tuned exponential (factor default 2, maxInterval default Infinity); function → full control.
argsArgs[]Arguments forwarded to fn (after the signal) each poll. Read fresh per tick.
initialDataTSeed for data before the first success. Status still starts "idle".
onSuccess(data: T) => voidCalled after a successful poll (non-superseded, while polling). Fired from the event turn, never inside a state updater.
onError(error: E) => voidCalled after a failed poll. The abort of a paused/stopped/unmounted poll is never reported here.

Return — UsePollingReturn<T, E>

FieldTypeDescription
dataT | undefinedThe most recent successfully-resolved value. Retained across later pending/error transitions.
errorE | undefinedThe error from the most recent failed poll. Cleared when a poll starts and on success.
status"idle" | "pending" | "success" | "error"The lifecycle status of the latest poll — the source of truth.
isLoadingbooleanConvenience mirror of status === "pending".
isPollingbooleanWhether the loop is active — i.e. enabled && !paused.
pause / stop() => voidHalt the loop: no new polls, clear the pending timeout, abort the in-flight poll. Stable. (stop is an alias of pause.)
resume / start() => voidRestart the loop (respecting immediate). No effect while enabled is false. Stable. (start is an alias of resume.)

Behavioural guarantees (by design)

  • No overlapping polls: the next tick is scheduled with setTimeout after the current poll settles, so a slow fn never stacks in-flight requests (unlike a naive setInterval).
  • Controls vs. gate precedence: enabled is the declarative master switch; pause/resume are the imperative override within an enabled session. isPolling === enabled && !paused. While enabled is false, resume() is inert.
  • Pausing a pending poll: pause/stop/enabled:false/unmount abort the in-flight poll via its AbortSignal and discard its result. If a poll was in flight, status settles back to its last resolved value (success if there was data, else idle) and isLoading clears — it never sticks on pending.
  • Backoff: the delay grows only on consecutive failures (baseInterval * factor ** failureCount, clamped to maxInterval) and resets to interval the moment a poll succeeds.
  • What restarts the loop: only enabled and pause/resume state. A changed interval, args, backoff, callbacks, or inline fn are read through refs and apply on the next tick without tearing the loop down.
  • StrictMode: the double-invoked mount effect tears its first loop down (clearing the timeout, aborting in-flight) before starting the second, so exactly one self-scheduling loop is ever live.

Exported types

UsePollingOptions<T, Args, E>, UsePollingReturn<T, E>, PollingBackoff, BackoffOptions, BackoffFn, plus the pure helper computePollingDelay(failureCount, baseInterval, backoff?), the DEFAULT_POLLING_INTERVAL constant, and the shared AsyncStatus, AsyncState<T, E>, AsyncFn<T, Args>, AsyncFnWithSignal<T, Args> re-exported from the async siblings.

Go deeper

This page is the quick reference. For every example, prop, and edge case, read the full README — or open Storybook to change props live.

More in timing & async