Skip to content
react-mediadrop
Esc
navigateopen⌘Jpreview
On this page

Writing a custom transport

Implement the UploadTransport contract for anything the reference XHR transport doesn't cover

react-mediadrop/xhr-upload covers a generic REST-ish endpoint. For anything else — a provider SDK, a test double, a more advanced protocol — write your own transport against the same contract:

type UploadTransport = {
	upload(
		file: MediaDropFile,
		context: {
			onProgress: (progress: { loaded: number; total: number | null }) => void;
			signal: AbortSignal;
		},
	): Promise<{ response?: unknown }>;
};

What must a transport do?

  • One file, one attempt. Send the file, report progress, resolve or reject. The queue decides whether to retry — you don’t.
  • Call onProgress as the upload progresses. Use total: null when the length can’t be determined.
  • Wire signal’s abort event to whatever cancellation your transport has (e.g. XMLHttpRequest.abort(), fetch’s own signal support).
  • Resolve with { response } — anything, opaque to the engine (e.g. the server’s parsed JSON body) — on success. Reject on failure.
  • Do not implement your own retry or backoff inside the transport. Use the shared withRetry engine (re-exported from react-mediadrop) for any finer-grained retry the transport itself needs — see Upload. This is a direct reaction to a real anti-pattern: libraries that let every transport carry its own independent copy of retry/backoff logic end up with subtly different retry behavior per transport, and no single place to fix a bug in it. react-mediadrop has one retry engine, and this is where you plug into it.
  • Do not implement your own concurrency limit. The queue decides how many uploads run at once; your transport just services one call at a time when asked.

What does a minimal transport look like?

import { withRetry, type UploadTransport } from "react-mediadrop";

function createMyTransport(options: { endpoint: string }): UploadTransport {
	return {
		async upload(file, { onProgress, signal }) {
			const response = await fetch(options.endpoint, {
				method: "POST",
				body: file.file,
				signal,
			});

			if (!response.ok) {
				throw new Error(`Upload failed: ${response.status}`);
			}

			onProgress({ loaded: file.size, total: file.size });
			return { response: await response.json() };
		},
	};
}

This example reports progress only once, at completion, because fetch has no cross-browser upload-progress API — this is exactly why react-mediadrop/xhr-upload uses XMLHttpRequest instead. If your task needs real incremental progress, use XMLHttpRequest.upload.onprogress the way the reference transport does — see its API reference.

What about multi-request transports?

A multi-request transport (splitting one file into several requests, like a resumable protocol) is still “one file, one upload() call” from the queue’s point of view — internally it may issue many requests and retry individual ones via the shared withRetry, called again for that finer-grained retry, but never a second, hand-rolled retry implementation.

Resumable transports (S3 multipart, tus) aren’t part of this codebase today — see Roadmap.

Was this page helpful?