Skip to content

Vue useProgress Composable

A reactive composable for tracking progress of multiple async operations. Maintains a count of total and completed promises, exposing a proportion (0–1) for driving progress bars and other UI feedback.

When to use

Use this composable when you need to show progress for batch async work — file uploads, bulk API calls, or any set of parallel promises. It exposes a reactive proportion (0–1) that drives progress bars and loading indicators.

The pattern

Create a progress tracker from a reactive source of promises. The returned object exposes total, completed, and proportion as computed properties that update as promises settle.

With batch operations

Feed promises into the tracker as operations start. Use proportion to drive a progress bar width.

javascript
import { useProgress } from "./use-progress.js";

const model = reactive({
  promises: [],
});

const progress = useProgress(() => model.promises);

// Add promises as operations start
async function processItems(items) {
  for (const item of items) {
    const p = processItem(item);
    model.promises.push(p);
  }
}

The template binds proportion to a progress bar width and displays the count so the user sees both a visual indicator and exact numbers.

vue
<template>
  <div class="ProgressBar">
    <div
      class="Fill"
      :style="{
        width: `${progress.proportion * 100}%`,
      }"
    />
  </div>
  <p>{{ progress.completed }} / {{ progress.total }}</p>
</template>

With a loading indicator component

Dispatch proportion updates to a state machine for coordinating loading UI.

javascript
const progress = useProgress(props.promises);

watchEffect(() => {
  stateMachine.dispatch({
    type: "progress",
    progress: progress.proportion,
  });
});

Implementing useProgress

Accept a reactive source of promises. Track each promise's fulfillment status in a reactive array. Derive total, completed, and proportion as computed properties.

javascript
import { computed, reactive } from "vue";

export function useProgress(promisesSource) {
  const archivedPromises = new WeakSet();
  const currentPromises = reactive([]);
  
  const progress = reactive({
    total: computed(
      () => currentPromises.length,
    ),
    completed: computed(
      () => currentPromises.filter(
        (p) => p.fulfilled,
      ).length,
    ),
    proportion: computed(() => {
      if (currentPromises.length === 0) return 0;
      const done = currentPromises.filter(
        (p) => p.fulfilled,
      ).length;
      return done / currentPromises.length;
    }),
  });
  
  // Watch the promises source and track new entries
  // Each promise entry tracks its fulfilled state
  // Archive completed batches via WeakSet
  
  return progress;
}

WeakSet for archiving

Completed promise batches are archived in a WeakSet so they can be garbage collected. This prevents memory leaks when running multiple sequential batches.

Reactive array tracking

Each promise is wrapped in a reactive entry with a fulfilled flag. When the promise settles, the flag flips and the computed properties update automatically.

The proportion computed

The computed returns a 0–1 value suitable for CSS width, transform: scaleX() or <progress> elements. It returns 0 when there are no promises to track. The promisesSource parameter is a function (not a static array) so it can return a reactive reference that changes over time.

Trade-offs

  • Promise-based only — Tracks promises, not arbitrary progress values. For operations that report intermediate progress (like file upload percentage), a different mechanism is needed.
  • No error tracking — A fulfilled flag tracks completion but doesn't distinguish success from failure. Failed promises count as incomplete unless the tracking logic catches rejections.
  • Sequential batches — The WeakSet archiving pattern works for sequential batches of work. If promises from overlapping batches are mixed, the counts may be confusing.