Skip to content

File lifecycle

Each file is an UploadFile with a status, progress, optional error, and optional adapter response. The UI should render from this object — not from ad-hoc flags.

State machine

pending  →  uploading  →  success
                       →  error       →  (retryFile)  →  uploading
                       →  cancelled   →  (retryFile)  →  uploading
StatusMeaning
pendingAccepted, not yet started. Waiting for upload() or autoUpload.
uploadingIn flight (or queued behind the concurrency cap).
successAdapter resolved. response is set.
errorAdapter rejected after retries exhausted. error is set.
cancelledcancelFile / cancelAll / removeFile abort.

Automatic retries (maxRetries) happen inside the queue while the item is still considered in-flight. The file does not flicker through error between automatic attempts. User-facing retryFile is for files that already landed on error or cancelled.

Starting uploads

  • autoUpload: trueaddFiles enqueues immediately.
  • autoUpload: false (default) — files sit in pending until you call upload().

Use manual mode when the user should review the list, fill a form, or confirm before bytes leave the browser.

tsx
const uploader = useUploader({ adapter, autoUpload: false });

<button onClick={uploader.upload} disabled={uploader.isUploading}>
  Upload {uploader.files.filter((f) => f.status === 'pending').length} files
</button>

Concurrency

concurrency (default 3) is the maximum number of adapters running at once. The rest wait in an in-memory queue.

Raise it for many small files on a fast link. Lower it for large videos or a backend that rate-limits. This is client-side only; it does not replace server quotas.

Retry

Two layers:

  1. AutomaticmaxRetries + retryDelay. Delay is exponential: retryDelay * 2^attempt. Default maxRetries is 0 (fail once).
  2. ManualretryFile(id) / retryAll() for error and cancelled.

retryCount on UploadFile increments on manual retry. Use it in the UI if you want “retried 2×”.

Do not retry blindly on 4xx. If the adapter can distinguish “payload too large” from “timeout”, throw a typed error and skip auto-retry for the former (maxRetries: 0, let the user fix the file).

Cancel vs remove vs clear

MethodIn-flight requestFile stays in files
cancelFile / cancelAllAbortedYes, status cancelled
removeFileAbortedNo
clearCompletedUntouchedRemoves success only
clearAllAbortedEmpties the list

removeFile is the right action for a trash icon. cancelFile is for “stop this upload but keep the row”. clearCompleted is a tidy-up after a batch.

The adapter must listen to signal. If it ignores abort, cancel will update UI status while the request keeps running.

Progress

  • Per file: file.progress (0–100), driven by onProgress in the adapter.
  • Aggregate: totalProgress is the average across all files, including completed ones.

If you need “average of active uploads only”, compute it yourself from files.filter(f => f.status === 'uploading'). The built-in value is a simple overall bar.

fetch cannot feed onProgress. Either use XHR or treat progress as indeterminate (isUploading + a spinner).

Callbacks

CallbackWhen
onFileAddedAfter a file is accepted into state
onFileRemovedAfter removeFile
onUploadStartAdapter is about to run
onUploadProgressEach onProgress tick
onUploadSuccessAdapter resolved
onUploadErrorAdapter rejected (after auto-retries)
onAllCompleteQueue drained (no active or waiting items)

Callbacks are stored in a ref, so you can pass inline functions without resetting the queue. Still keep them cheap; onUploadProgress can fire often.

onAllComplete fires when the queue goes idle — including after cancels that empty the active set. If you need “all succeeded”, check files.every(f => f.status === 'success') inside the callback.

UploadFile shape

ts
interface UploadFile<TResponse> {
  id: string;
  file: File;
  status: 'pending' | 'uploading' | 'success' | 'error' | 'cancelled';
  progress: number;
  error: Error | null;
  response: TResponse | null;
  retryCount: number;
}

id is generated by the library. Use it as a React key and as the argument to cancel/retry/remove.

Released under the MIT License.