react-mediadrop/xhr-upload ships as the reference transport.
Resumable transports (S3 multipart, tus) aren’t available today — see
Roadmap.
How does upload work?
- You pick a transport — a small object with one method,
upload(file, { onProgress, signal }).createXhrUploadTransport()is the reference implementation. You can write your own for anything else (a provider SDK, a test double, a more advanced resumable protocol). - You pass that transport to
useMediaDrop({ transport, ... })— the exact same option regardless of which transport it is. - Only then do
uploadFile/uploadAll/cancelUpload/cancelAllUploads/retryUploadexist on the returned object — withouttransport, they are absent, and TypeScript will not let you call them. This mirrors Core’s own restraint: no feature exists halfway. - Every file’s upload progress lives on the
MediaDropFileitself (uploadStatus,progress,uploadError,uploadResult,uploadAttempts) — you read it the same way you already readstatus/errors, viafiles.
Where does retry and concurrency logic live?
react-mediadrop owns all upload orchestration. A transport’s job is
exactly one thing: send one file, once, report progress, resolve or
reject. It has no retry loop and no concurrency limit of its own —
both are the queue’s job. useMediaDrop’s upload methods are thin
pass-throughs to the same queue.
Writing your own transport? Retry and concurrency stay out of it — see
Writing a custom transport for the shared
withRetry engine it should call into instead.
The shared retry engine
withRetry(attempt, options, signal) is the one retry engine, used by the
queue and available to any transport that needs finer-grained retry of
its own:
defaultShouldRetry, the built-in default, retries 408, 429, and every
5xx status, plus anything without a recognizable HTTP status (network
errors) — it does not retry other 4xx statuses (400/401/403/404/413, etc.),
since those describe a request that fails the same way every time. Pass
your own shouldRetry to override this classification entirely. jitter
matters when many requests could fail at once (e.g. every part of a
multipart upload hitting the same transient network issue) — it spreads
their retries out instead of having them all retry in lockstep.
What upload fields does MediaDropFile have?
uploadError.code is always "upload-error". uploadError.status (HTTP
status) and uploadError.sourceCode (a transport’s own finer-grained
error classification, if it attaches one) are both optional. Don’t switch
exhaustively on sourceCode; it’s transport-specific and open-ended.
uploadStatus is undefined until an upload is requested for that
file — a freshly-accepted file has no uploadStatus at all, not
"queued". It only ever applies to status: "accepted" files.
status and uploadStatus stay separate
status ("idle" | "accepted" | "rejected") is the Core validation
verdict and is never touched by the upload queue — it’s decided once,
when the file is added. uploadStatus is a completely independent field
for the upload lifecycle:
getAcceptedFiles()/getRejectedFiles()behave identically whether or not any upload has started, finished, or failed.maxFilescounting (based onstatus) is unaffected by upload progress — a file finishing its upload does not free up amaxFilesslot.
How do I configure concurrency, retry, and cancel?
uploadFile(id)— queues a file (or restarts it, even if it already finished/failed/was canceled). No-op if the file isn’tstatus: "accepted"or is already in flight.uploadAll()— queues every currentlystatus: "accepted"file.cancelUpload(id)— aborts it if it’s uploading (viaAbortSignal, standard web API), or drops it if it’s merely queued. Ends inuploadStatus: "canceled", not"error".cancelAllUploads()— cancels every queued and in-flight file.retryUpload(id)— re-enqueues a file, but only if its last attempt ended inuploadStatus: "error"— a no-op otherwise. This is a manual retry after automatic retries were exhausted, distinct from the automaticretriesconfig above.
cancelGraceMs (default 5000) is a safety net, not the normal
path: a well-behaved transport wires up signal and rejects promptly
once aborted, so cancel usually settles almost immediately. If a
transport doesn’t, this timer force-frees the slot after the grace
period regardless.removeFile(id)/clearFiles() cancel any in-flight upload for the
files they remove — no orphaned request keeps running in the
background with a leaked concurrency slot.
What’s the transport contract?
What are the session-persistence utilities for?
react-mediadrop still exports the metadata-persistence utilities built
for resumable transports, even though no transport in this codebase
currently uses them:
createFileFingerprint is
metadata-based on purpose: hashing file contents would let two
selections of a huge file be compared reliably, but reading the whole
file to do that is exactly the cost react-mediadrop avoids imposing by
default. Two different files with identical name, size, type, modified
time, and relative path will still collide — this is “looks like the
same file,” not a guarantee.
Writing a custom transport
Implement the transport contract yourself
Core concepts
The file model, the store, and drag state