Skip to main content

Upload Client

5 min read

@upstash/blob/react is the browser side of a direct upload. uploadHooks binds the hooks to your upload handler, and useUpload runs the upload and reports its progress. There is a plain function for apps without React, and useServerUpload for routes where the bytes do pass through your app.

lib/upload-hooks.ts
app/page.tsx

uploadHooks#

lib/upload-hooks.ts

uploadHooks<typeof uploads>(defaults) binds useUpload to one handler. The bound hook knows the route names, so a typo does not compile, and it knows each route's input and completion data. Called with no type parameter, uploadHooks() returns an unbound useUpload that takes a URL instead of a route name.

Every option is optional.

OptionDefaultDescription
headersnoneA function returning headers to send to your route. Re-read on every request.
concurrency3How many files upload at once.
endpoint'/api/upload'Where the handler is mounted.
onErrornoneRuns for every failed upload.

A call-site option on useUpload wins over the default, except onError, where the default runs first and the call-site one after it.

Warning

A call-site onError must not throw. A throw there stops the rest of the upload queue from starting. A throw from the configured default onError is caught and logged.


useUpload#

app/page.tsx
ReturnsDescription
startBegins one upload or several. Returns the record(s).
uploadsEvery record, in the order they were started.
uploadThe newest record, or null.
clear(id?)Removes one record, or all of them.
acceptThe route's contentTypes, joined, for an <input accept>.
constraintsWhat the route's GET served. undefined until it answers.
app/page.tsx

start({ file }) returns one record, or null when the file is nullish, so an empty file picker is not an error. start({ files }) takes a File[] or a FileList and returns an array.

Three files upload at once by default and the rest queue. clear(id?) removes records from the list; a cleared upload that is still running finishes anyway. Unmounting the component does not cancel anything either.

The record#

FieldTypeDescription
idstringStable for the life of the record. Use it as the list key.
fileFileThe file this record uploads.
status'queued' | 'uploading' | 'finishing' | 'paused' | 'done' | 'canceled' | 'error'
loadednumberBytes that have landed.
totalnumberThe file's size.
percentnumber0 to 99 while running, 100 only once done.
pendingbooleanNot settled: queued, uploading, finishing or paused.
stalledbooleanEvery request in flight is waiting on a backoff.
canPausebooleanWhether pause() would do anything.
blobCompletedBlob & { data }On done only.
errorBlobErrorOn error only.
pause() resume() cancel() retry()() => booleanEach answers whether it did anything.

Drive UI off pending rather than deriving it from status. percent stays at 99 through finishing, because 100 means stored, not sent. See Progress and status.

blob.data is typed from that route's onUploadComplete. Fields a status does not carry are undefined rather than absent, so upload?.blob?.url and upload?.error?.message read straight off the record with no narrowing.

canPause is false for a single PUT, which is every file under the multipart threshold. retry() works only from error, and resumes from the parts that already landed. See Large files.

headers#

app/page.tsx

headers is a function, not an object. It is called before every request the SDK makes to your route, so a token that rotates mid-upload keeps working.

A throw from it fails the upload with that error and no retry. Use this to refuse an upload from the app side, for example when a token could not be refreshed.


Without React#

app/uploader.ts

upload() starts immediately and returns an UploadTask: snapshot() for the current state, subscribe() for changes, done as a promise, and pause(), resume(), cancel() and retry(). The snapshot has the same fields as the React record.


useServerUpload#

app/api/avatar/route.ts
app/avatar.tsx

For bytes that must pass through your app, do not use an upload handler. Write an ordinary route that calls bucket.put, and drive it with useServerUpload: one POST, with upload progress, cancellation and BlobError decoding. The route's JSON comes back as response.

Every option is optional.

OptionDefaultDescription
field'file'The form field the file is sent under. Must match what your route reads.
headersnoneA function returning headers to send with the request.
concurrency3How many files upload at once.
onDonenoneRuns for every upload that finishes.
onErrornoneRuns for every failed upload.

start({ body }) sends a File, Blob or FormData as the raw body instead of a form field. The record has cancel() only and no pause, since there is no multipart. Statuses are queued, uploading, finishing, done, canceled and error.

A proxied upload is capped by your platform's request body limit rather than by maxSize. The SDK surfaces that refusal as too_large with the platform's limit as a hint; see Platform body limits. Anything larger needs a direct upload with an upload handler.