How to upload multiple files to Vercel Blob with progress trackers

I’m basically trying to replicate what Vercel Blob itself does in the screenshot above, allow multiple file uploads at once and track the progress of each.

I handled it like this and it seems to work ok, but it’s one of those things that I’d really love to see an official example or at least get more eyes on.

  async function handleSubmit(e: FormEvent<HTMLFormElement>) {
    e.preventDefault();

    if (files.length) {
      setIsUploading(true);

      try {
        // Upload files concurrently
        const uploadPromises = files.map(async (fileProgress, index) => {
          if (fileProgress.status !== "pending") return;
          try {
            // Update status to uploading
            setFiles((prev) =>
              prev.map((f, i) =>
                i === index ? { ...f, status: "uploading" as const } : f
              )
            );

            // Upload the fie
            const blob = await upload(
              fileProgress.file.name,
              fileProgress.file,
              {
                access: "public",
                handleUploadUrl: "/api/upload",
                multipart: true,
                onUploadProgress: ({ percentage }) => {
                  // Update the progress
                  setFiles((prev) =>
                    prev.map((f, i) =>
                      i === index ? { ...f, progress: percentage } : f
                    )
                  );
                },
              }
            );

            // Update status to completed
            setFiles((prev) =>
              prev.map((f, i) =>
                i === index
                  ? {
                      ...f,
                      status: "completed" as const,
                      url: blob.url,
                      progress: 100,
                    }
                  : f
              )
            );
          } catch (error) {
            // Update status to error
            setFiles((prev) =>
              prev.map((f, i) =>
                i === index
                  ? {
                      ...f,
                      status: "error" as const,
                      error:
                        error instanceof Error
                          ? error.message
                          : "Upload failed",
                    }
                  : f
              )
            );
          }
        });

        await Promise.allSettled(uploadPromises);
        setIsUploading(false);
        notifications.show({
          color: "green",
          title: "File uploaded!",
          message: `Your files have been uploaded`,
        });
      } catch (error) {
          notifications.show({
            color: "red",
            title: "Error",
            message: error.message,
          });
      }
    }
  }

Hey, Justin! Welcome :slight_smile:

What are you building? :eyes:

Your approach is looking great so far — you’ve got the key pieces down: using Promise.allSettled() for concurrent uploads, tracking individual progress with onUploadProgress, and managing state cleanly for each file.

A couple of ideas that could make it even more robust:

Error handling: You could add retry logic for any failed uploads. Something like:

const uploadWithRetry = async (file, maxRetries = 3) => {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await upload(file.name, file, options);
    } catch (error) {
      if (attempt === maxRetries) throw error;
      await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
    }
  }
};

Performance: If you’re uploading lots of files, it might help to limit how many go through at once. For example:

const batchSize = 3;
for (let i = 0; i < files.length; i += batchSize) {
  const batch = files.slice(i, i + batchSize);
  await Promise.allSettled(batch.map(uploadFile));
}

Cleanup tip: Make sure you’re handling component unmounts properly to avoid any memory leaks.

You’re on the right track! We don’t have a Vercel example for this exact use case, but what you’ve built aligns well with React patterns. Keep going! This is shaping up nicely. If you’d like to contribute one, we’d always welcome them :slight_smile: