XHR with progress
XMLHttpRequest is the right default when the UI needs a percent. xhr.upload exposes request-body progress; fetch does not.
ts
import type { UploadAdapter } from 'react-upload-kit';
interface UploadResponse {
url: string;
}
export const xhrAdapter: UploadAdapter<UploadResponse> = (file, { onProgress, signal }) => {
const body = new FormData();
body.append('file', file);
const xhr = new XMLHttpRequest();
return new Promise((resolve, reject) => {
const onAbort = () => {
xhr.abort();
reject(new DOMException('Upload aborted', 'AbortError'));
};
if (signal.aborted) {
onAbort();
return;
}
signal.addEventListener('abort', onAbort, { once: true });
xhr.upload.addEventListener('progress', (event) => {
if (event.lengthComputable) {
onProgress(Math.round((event.loaded / event.total) * 100));
}
});
xhr.addEventListener('load', () => {
signal.removeEventListener('abort', onAbort);
if (xhr.status >= 200 && xhr.status < 300) {
resolve(JSON.parse(xhr.responseText) as UploadResponse);
} else {
reject(new Error(`Upload failed: ${xhr.status}`));
}
});
xhr.addEventListener('error', () => {
signal.removeEventListener('abort', onAbort);
reject(new Error('Network error'));
});
xhr.open('POST', '/api/upload');
xhr.send(body);
});
};Wire it in:
tsx
const uploader = useUploader<UploadResponse>({
adapter: xhrAdapter,
autoUpload: true,
concurrency: 3,
maxRetries: 2,
});Notes
- Call
onProgresswith integers0–100. The library does not clamp; keep the value in range. - Listen to
signaleven if you never show a Cancel button —removeFileandclearAllabort too. - Parse and type the JSON yourself. The adapter is the boundary; do not leak
XMLHttpRequestinto React state.
See Adapters for fetch vs XHR vs S3.