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.