@usefy/use-polling
v0.25.1
Published
A React hook to poll an async function on an interval — non-overlapping self-scheduling ticks, pause/resume, an enabled gate, and exponential backoff
Downloads
1,405
Maintainers
Readme
Overview
usePolling is part of the @usefy ecosystem — a collection of production-ready, TypeScript-first, SSR-safe React hooks. It polls an async function on an interval, exposing the latest result as { data, error, status, isLoading } (the same shape as useAsyncFn / useAsync) plus polling-loop control: isPolling and imperative pause/resume (with start/stop aliases).
The critical property: the next tick is scheduled only after the current poll settles — a self-rescheduling setTimeout, never a stacking setInterval — so a fn slower than the interval can never overlap requests.
Features
- No overlapping polls — at most one poll in flight; the next is scheduled only after the current one settles (self-scheduling
setTimeout) - pause / resume (and
start/stopaliases) — imperative control;isPollingreflects whether the loop is running enabledgate — declarative master switch; whilefalsethe hook never polls andresume()can't start itimmediate— the first poll fires right away by default (immediate: falsewaits one interval)- Exponential backoff — grow the delay after consecutive failures (
true,{ factor, maxInterval }, or a custom(failures, base) => ms), reset on the next success - AbortController — your
fnreceives anAbortSignalfirst; the in-flight poll is aborted on pause/stop/enabled:false/unmount, and a stale-guard discards a torn-down poll's late result - Same async state as the family —
{ data, error, status, isLoading }wherestatusis the source of truth,isLoading === status === "pending", anddatais retained on error - SSR-safe & StrictMode-safe — no timers/
AbortControllerat module/render time; exactly one self-scheduling loop under StrictMode (no runaway duplicate timers) - Stable controls & latest refs — controls are memoized; an inline
fnand changing options are read through refs, so they never restart the loop or go stale - TypeScript-first — full type inference; reuses the shared
AsyncStatus/AsyncState/AsyncFnWithSignaltypes
Installation
# npm
npm install @usefy/use-polling
# yarn
yarn add @usefy/use-polling
# pnpm
pnpm add @usefy/use-pollingRequires React 18 or 19 (peerDependencies: "react": "^18.0.0 || ^19.0.0").
Quick Start
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
const {
data, error, status, isLoading,
isPolling, pause, resume, start, stop,
} = usePolling<T, Args, E>(fn, options?);Parameters
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| 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. |
| options | UsePollingOptions<T, Args, E> | Optional. See below. |
Options — UsePollingOptions<T, Args, E>
| Option | Type | Default | Description |
| ------ | ---- | ------- | ----------- |
| interval | number | 1000 | Base delay (ms) between one poll settling and the next starting. Applies from the next tick — never restarts the loop. |
| immediate | boolean | true | Poll immediately when the loop starts/resumes, vs. waiting one interval. |
| enabled | boolean | true | Declarative master gate. false → never polls; flipping to true (re)starts the loop. While false, resume() cannot start polling. |
| backoff | boolean \| { factor?, maxInterval? } \| (failures, base) => number | false | Grow the delay on consecutive failures, reset on success. true → exponential factor: 2; object → tuned exponential (factor default 2, maxInterval default Infinity); function → full control. |
| args | Args | [] | Arguments forwarded to fn (after the signal) each poll. Read fresh per tick. |
| initialData | T | — | Seed for data before the first success. Status still starts "idle". |
| onSuccess | (data: T) => void | — | Called after a successful poll (non-superseded, while polling). Fired from the event turn, never inside a state updater. |
| onError | (error: E) => void | — | Called after a failed poll. The abort of a paused/stopped/unmounted poll is never reported here. |
Return — UsePollingReturn<T, E>
| Field | Type | Description |
| ----- | ---- | ----------- |
| data | T \| undefined | The most recent successfully-resolved value. Retained across later pending/error transitions. |
| error | E \| undefined | The 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. |
| isLoading | boolean | Convenience mirror of status === "pending". |
| isPolling | boolean | Whether the loop is active — i.e. enabled && !paused. |
| pause / stop | () => void | Halt the loop: no new polls, clear the pending timeout, abort the in-flight poll. Stable. (stop is an alias of pause.) |
| resume / start | () => void | Restart 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
setTimeoutafter the current poll settles, so a slowfnnever stacks in-flight requests (unlike a naivesetInterval). - Controls vs. gate precedence:
enabledis the declarative master switch;pause/resumeare the imperative override within an enabled session.isPolling === enabled && !paused. Whileenabledisfalse,resume()is inert. - Pausing a pending poll:
pause/stop/enabled:false/unmount abort the in-flight poll via itsAbortSignaland discard its result. If a poll was in flight,statussettles back to its last resolved value (successif there was data, elseidle) andisLoadingclears — it never sticks onpending. - Backoff: the delay grows only on consecutive failures (
baseInterval * factor ** failureCount, clamped tomaxInterval) and resets tointervalthe moment a poll succeeds. - What restarts the loop: only
enabledandpause/resumestate. A changedinterval,args,backoff, callbacks, or inlinefnare 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.
Testing
📊 View Detailed Coverage Report (GitHub Pages) — 39 tests, 97% statement coverage.
License
MIT © mirunamu
This package is part of the usefy monorepo.
