Spoosh
Hooks

useQueue

Manage concurrent request queues with progress tracking

The useQueue hook enables concurrent request processing with built-in queue management, progress tracking, and abort/retry capabilities. Ideal for batch uploads, bulk operations, or any scenario requiring controlled concurrent requests.

Basic Usage

function FileUploader() {
  const { trigger, tasks, stats, clear } = useQueue(
    (api) => api("uploads").POST(),
    { concurrency: 3 }
  );

  const handleUpload = async (files: File[]) => {
    for (const file of files) {
      trigger({ body: { file, filename: file.name } })
        .then((result) => console.log("Uploaded:", result.data))
        .catch((err) => console.error("Failed:", err));
    }
  };

  return (
    <div>
      <input type="file" multiple onChange={(e) => handleUpload([...e.target.files])} />
      <p>Progress: {stats.percentage}%</p>
      <p>Running: {stats.running} / Pending: {stats.pending}</p>
      <button onClick={clear}>Cancel All</button>
    </div>
  );
}

Queue Options

OptionTypeDefaultDescription
concurrencynumber3Maximum concurrent requests
+ plugin opts--Hook-level options from plugins
const queue = useQueue((api) => api("uploads").POST(), {
  concurrency: 5,
  retry: { retries: 3 }, // from retry plugin
  progress: true, // from progress plugin
});

Returns

PropertyTypeDescription
trigger(input?) => Promise<Response>Add item to queue and execute, returns when complete
tasksQueueItem[]All tasks with their current status
statsQueueStatsQueue statistics
abort(id?) => voidAbort task by ID, or all if no ID
retry(id?) => Promise<void>Retry failed task by ID, or all failed
remove(id: string) => voidRemove specific task by ID (aborts if active)
removeSettled() => voidRemove all settled tasks (keeps pending/running)
clear() => voidAbort all and clear entire queue
setConcurrency(n: number) => voidUpdate concurrency limit at runtime

QueueItem Properties

Each task in the tasks array has:

PropertyTypeDescription
idstringUnique task identifier
status"pending" | "running" | "success" | "error" | "aborted"Current status
dataTData | undefinedResponse data on success
errorTError | undefinedError on failure
inputobject | undefinedOriginal trigger input
metaobject | undefinedPlugin metadata (e.g., progress)

QueueStats Properties

PropertyTypeDescription
pendingnumberItems waiting to run
runningnumberCurrently executing items
settlednumberCompleted items (success + failed)
successnumberSuccessfully completed items
failednumberFailed or aborted items
totalnumberTotal items in queue
percentagenumberCompletion percentage (0-100)

Trigger Options

OptionTypeDescription
idstringCustom ID for the task (auto-generated if unset)
bodyTBodyRequest body
queryTQueryQuery parameters
paramsRecord<string, string | number>Path parameters
+ plugin options-Options from installed plugins

Dynamic Concurrency

Adjust concurrency at runtime using setConcurrency:

function UploaderWithDynamicConcurrency() {
  const [concurrency, setConcurrency] = useState(3);

  const { trigger, stats, setConcurrency: setQueueConcurrency } = useQueue(
    (api) => api("uploads").POST(),
    { concurrency }
  );

  return (
    <div>
      <label>
        Concurrency:
        <input
          type="number"
          value={concurrency}
          onChange={(e) => setConcurrency(Number(e.target.value))}
          min={1}
          max={10}
        />
        <button onClick={() => setQueueConcurrency(concurrency)}>
          Apply
        </button>
      </label>
      <p>Running: {stats.running} / Pending: {stats.pending}</p>
    </div>
  );
}

When decreasing concurrency, already running tasks will complete, but new tasks won't start until the running count drops below the new limit.

Progress Tracking

With the progress plugin, track individual upload progress:

const { trigger, tasks } = useQueue((api) => api("uploads").POST(), {
  concurrency: 2,
  progress: true,
});

// In your UI
{tasks.map((task) => (
  <div key={task.id}>
    <span>{task.input?.body?.filename}</span>
    <progress value={task.meta?.uploadProgress ?? 0} max={100} />
    <span>{task.status}</span>
  </div>
))}

Abort and Retry

const { trigger, tasks, abort, retry, clear } = useQueue((api) =>
  api("uploads").POST()
);

// Abort specific task
abort(taskId);

// Abort all running/pending tasks
abort();

// Retry specific failed task
await retry(taskId);

// Retry all failed tasks
await retry();

// Abort all and clear queue
clear();

Removing Completed Tasks

const { tasks, remove, removeSettled } = useQueue((api) =>
  api("uploads").POST()
);

// Remove specific task
remove(taskId);

// Remove all finished tasks (keeps running/pending)
removeSettled();

Batch Operations Example

function BulkDeleteUsers() {
  const { trigger, stats, clear } = useQueue(
    (api) => api("users/:id").DELETE(),
    { concurrency: 5 }
  );

  const handleBulkDelete = async (userIds: string[]) => {
    const promises = userIds.map((id) =>
      trigger({ params: { id } })
    );

    const results = await Promise.allSettled(promises);
    console.log(`Deleted ${stats.success} of ${stats.total} users`);
  };

  return (
    <div>
      <button onClick={() => handleBulkDelete(selectedIds)}>
        Delete Selected ({selectedIds.length})
      </button>
      {stats.total > 0 && (
        <div>
          <progress value={stats.percentage} max={100} />
          <span>{stats.percentage}% complete</span>
          <button onClick={clear}>Cancel</button>
        </div>
      )}
    </div>
  );
}

Task List UI Example

function UploadQueue() {
  const { trigger, tasks, stats, abort, retry, remove, removeSettled, clear } = useQueue(
    (api) => api("uploads").POST(),
    { concurrency: 3 }
  );

  return (
    <div>
      <div className="stats">
        <span>Progress: {stats.percentage}%</span>
        <span>Running: {stats.running}</span>
        <span>Pending: {stats.pending}</span>
        <span>Success: {stats.success}</span>
        <span>Failed: {stats.failed}</span>
      </div>

      <div className="actions">
        <button onClick={() => abort()} disabled={stats.running === 0}>
          Abort All
        </button>
        <button onClick={() => retry()} disabled={stats.failed === 0}>
          Retry Failed
        </button>
        <button onClick={removeSettled} disabled={stats.settled === 0}>
          Remove Finished
        </button>
        <button onClick={clear} disabled={stats.total === 0}>
          Clear All
        </button>
      </div>

      <ul className="task-list">
        {tasks.map((task) => (
          <li key={task.id}>
            <span className={`status-${task.status}`}>{task.status}</span>
            <span>{task.input?.body?.filename}</span>

            {task.status === "running" && (
              <button onClick={() => abort(task.id)}>Abort</button>
            )}
            {task.status === "error" && (
              <button onClick={() => retry(task.id)}>Retry</button>
            )}
            <button onClick={() => remove(task.id)}>Remove</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

On this page