A headless React hook and Vue 3 composable for file uploads with real progress tracking, retries, cancellation, and chunked uploads for large files.
fetch can't report upload progress — there's no way to know how many bytes of a file have actually been sent, only when the whole request finishes. useCourier uses XMLHttpRequest under the hood specifically to expose real 0-100% upload progress, wrapped in a small, typed, headless hook you build your own upload UI on top of.
- Real upload progress — accurate 0-100% progress per file as bytes are actually sent, not a spinner
- Retries — re-run a failed upload without asking the user to re-select the file
- Cancellation — abort an in-flight upload, or every upload in flight on unmount
- Chunked uploads — automatically splits large files into sequential requests, retrying failed chunks independently
- Authenticated APIs — send auth headers (including async tokens), cookies, and extra form fields with every request
- Lifecycle callbacks —
beforeUpload,onUploadSuccess,onUploadError,onUploadFinish,onUploadRetry,onRemoveFile - React and Vue — the same API as a React hook (
use-courier) and a Vue 3 composable (use-courier/vue) - Headless — no UI, no styling opinions; works with any component library
- Typed — full TypeScript support, generic over your API's response shape
npm install use-courieruse-courier supports React 18 and 19, and Vue 3.3 and later. Install only the framework you use.
import type { ChangeEvent } from "react";
import { useCourier } from "use-courier";
export function FileUpload() {
const { files, addFile } = useCourier({
url: "/api/uploads",
});
function handleFileChange(event: ChangeEvent<HTMLInputElement>) {
Array.from(event.target.files ?? []).forEach((file) => {
void addFile(file);
});
}
return (
<>
<input type="file" multiple onChange={handleFileChange} />
{files.map((item) => (
<p key={item.id}>
{item.file.name}: {item.status} ({item.uploadProgress}%)
</p>
))}
</>
);
}<script setup lang="ts">
import { useCourier } from "use-courier/vue";
const { files, addFile } = useCourier({ url: "/api/uploads" });
function handleFileChange(event: Event) {
const input = event.target as HTMLInputElement;
Array.from(input.files ?? []).forEach((file) => addFile(file));
}
</script>
<template>
<input type="file" multiple @change="handleFileChange" />
<p v-for="item in files" :key="item.id">
{{ item.file.name }}: {{ item.status }} ({{ item.uploadProgress }}%)
</p>
</template>Full API reference, backend integration guides for Express, Next.js, TanStack Start, and Hono, large-file chunking details, and a Shadcn attachments integration:
The docs also publish an llms.txt and llms-full.txt for LLM tooling.
MIT © Zach Philipp
