async-polling-task-manage
v1.0.0
Published
A React hook for managing async polling tasks with persistence, batch support, and automatic retry.
Maintainers
Readme
async-polling-task-manage
A lightweight React hook for managing async polling tasks — with persistence, batch support, and automatic retry.
Ideal for scenarios where you submit a backend task and need to poll its status until completion, even across page refreshes.
Features
- React Hook — Simple
usePollingTaskhook with minimal API surface - Persistence — Task state is saved to
localStorage; unfinished tasks automatically resume after page refresh - Batch Polling — Start multiple tasks in one call with
startBatch - Stateless Business Logic — You only provide a polling function; the hook manages all state internally
- Configurable — Interval, max attempts, timeout, error retry, all customizable per-task
- TypeScript — Full type definitions included
Install
npm install async-polling-task-manageQuick Start
import { usePollingTask } from "async-polling-task-manage";
function TaskPanel() {
const { tasks, start, startBatch, stop, isAnyPolling } = usePollingTask({
pollingFn: async (taskId) => {
const res = await fetch(`/api/tasks/${taskId}/status`);
const data = await res.json();
return {
done: data.status === "completed" || data.status === "failed",
data,
};
},
defaultOptions: {
interval: 2000,
},
});
return (
<div>
<button onClick={() => start("task-1")}>Start Task 1</button>
<button onClick={() => startBatch(["task-2", "task-3", "task-4"])}>
Start Batch
</button>
{Object.values(tasks).map((task) => (
<div key={task.taskId}>
<span>{task.taskId}</span>
<span>{task.status}</span>
<span>{JSON.stringify(task.result)}</span>
{task.status === "polling" && (
<button onClick={() => stop(task.taskId)}>Stop</button>
)}
</div>
))}
{isAnyPolling && <p>Polling in progress...</p>}
</div>
);
}API
usePollingTask<TResult>(config)
Config
| Property | Type | Required | Description |
| ---------------- | ----------------------------------------------- | -------- | ------------------------------------------------- |
| pollingFn | (taskId: string) => Promise<PollResponse<TResult>> | Yes | The function called on each poll cycle |
| namespace | string | No | Isolate task groups in storage (default: "default") |
| defaultOptions | PollingOptions | No | Default options applied to all tasks |
PollResponse<TResult>
interface PollResponse<TResult> {
done: boolean; // true = task finished, stop polling
data: TResult | null; // payload to store as result
}PollingOptions
| Option | Type | Default | Description |
| -------------- | --------- | ---------- | --------------------------------------------------- |
| interval | number | 3000 | Polling interval in milliseconds |
| maxAttempts | number | Infinity | Max poll attempts before timeout |
| timeout | number | Infinity | Max total time (ms) before timeout |
| persistent | boolean | true | Persist task state in localStorage |
| retryOnError | number | 3 | Consecutive errors allowed before marking as error |
Return Value
| Property | Type | Description |
| -------------- | ------------------------------------------------- | ------------------------------------ |
| tasks | Record<string, TaskState<TResult>> | All task states, keyed by taskId |
| start | (taskId: string, options?: PollingOptions) => void | Start polling a single task |
| startBatch | (taskIds: string[], options?: PollingOptions) => void | Start polling multiple tasks |
| stop | (taskId: string) => void | Stop polling a task (keeps state) |
| stopAll | () => void | Stop all active polling |
| remove | (taskId: string) => void | Remove a task's state entirely |
| clear | () => void | Clear all tasks and storage |
| isAnyPolling | boolean | Whether any task is currently polling |
TaskState<TResult>
interface TaskState<TResult> {
taskId: string;
status: "idle" | "polling" | "success" | "error" | "timeout";
result: TResult | null;
error: Error | null;
attempts: number;
createdAt: number;
lastPolledAt: number | null;
}Advanced Usage
Namespace Isolation
Use namespace to separate different groups of tasks. Each namespace has its own storage key.
const imageJobs = usePollingTask({
namespace: "image-processing",
pollingFn: checkImageStatus,
});
const videoJobs = usePollingTask({
namespace: "video-processing",
pollingFn: checkVideoStatus,
});Per-Task Options
Override default options for individual tasks:
start("urgent-task", {
interval: 500,
maxAttempts: 100,
retryOnError: 5,
});Non-Persistent Tasks
Disable persistence for tasks that don't need to survive refreshes:
start("ephemeral-task", { persistent: false });Page Refresh Recovery
Tasks with status: "polling" are automatically resumed when the hook mounts. No extra code needed — just use the same namespace and pollingFn.
License
MIT
