Adapters
The adapter is the only backend-facing contract in the library. Everything else — queue, retry, UI — is independent of how bytes leave the browser.
This is the right seam. Upload destinations change (your API, S3, tus, a vendor SDK). The file list and dropzone should not.
Contract
type UploadAdapter<TResponse> = (
file: File,
context: {
onProgress: (percent: number) => void;
signal: AbortSignal;
},
) => Promise<TResponse>;| Responsibility | How |
|---|---|
| Upload the file | Return a Promise that resolves with whatever your backend returns. |
| Report progress | Call onProgress with a 0–100 integer. Optional, but required for a useful progress bar. |
| Honour cancellation | Abort the request when signal fires. Throw or reject; the queue treats abort as cancel, not as a retryable error. |
| Surface failures | throw / reject with an Error. That becomes file.error. |
TResponse flows end-to-end: adapter → UploadFile.response → onUploadSuccess. Type it once on useUploader<TResponse>.
Keep the adapter stable
useUploader closes over adapter when enqueueing work. Recreating the function every render can surprise you (new identity, stale closures). Memoize it:
const adapter = useMemo(() => createXhrAdapter('/api/upload'), []);
const uploader = useUploader({ adapter });The demo does the same with a mock adapter so the queue always talks to one function instance.
Choose a transport
| Transport | Upload progress | Cancel | When to use |
|---|---|---|---|
XMLHttpRequest | Yes (xhr.upload) | xhr.abort() | Default for form uploads with a progress bar. |
fetch | No (request body progress is not exposed) | signal | Simple APIs where a spinner is enough. |
S3 presigned PUT | Yes, via XHR | Yes | Direct-to-bucket uploads; your API only mints the URL. |
| tus / resumable | Yes | Yes | Large files, flaky networks. Wrap the client in an adapter. |
fetch is shorter, but it cannot report upload progress. If the UI needs a percent, use XHR (or a wrapper around it). That is a browser limitation, not a library one.
Error vs abort
- Failure: reject with
Error. The file moves toerror. Automatic retries apply ifmaxRetries > 0. - Abort:
signalaborted. The file moves tocancelled. Automatic retries do not run; the user can callretryFile.
In the adapter, listen once and tear down:
signal.addEventListener('abort', () => xhr.abort());Do not treat AbortError as a business failure. The queue already distinguishes abort from a thrown error.
Typing the response
interface UploadResult {
url: string;
id: string;
}
const uploader = useUploader<UploadResult>({
adapter: async (file, ctx) => {
// ...
return { url: '...', id: '...' };
},
});
uploader.files[0]?.response?.url; // string | undefinedKeep the adapter return value small and stable. UI should not depend on transport-specific fields leaking into TResponse.
Next
- XHR adapter
- Fetch adapter
- S3 presigned URL
- File lifecycle — how adapter outcomes map to status