Pause, resume, and survive reloads and crashes.
"use client"
import * as React from "react"
import { MiB, createIndexedDBPersistence, s3Adapter } from "@uploadcn/core"
import { useUploadSelector, useUploader } from "@uploadcn/react"
import { HistoryIcon } from "lucide-react"
import { demoTransport } from "@/examples/_demo"
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
import {
Upload,
UploadDropzone,
UploadDropzoneDescription,
UploadDropzoneTitle,
UploadQueue,
} from "@/components/ui/upload"
/** Files ≥ 5 MB use multipart, so they resume from the last finished part. */
const adapter = s3Adapter({
endpoint: "/api/upload",
transport: demoTransport, // docs only, see examples/_demo.ts
multipart: { threshold: 5 * MiB, partSize: 5 * MiB },
})
export default function ResumableUploadExample() {
const [persistence] = React.useState(() =>
createIndexedDBPersistence({ name: "uploadcn-docs-resumable" })
)
const uploader = useUploader({ adapter, persistence, restore: true })
const restored = useUploadSelector(
uploader,
(state) =>
state.items.filter((item) => item.restored && item.status === "paused")
.length
)
return (
<Upload uploader={uploader}>
{restored > 0 ? (
<Alert>
<HistoryIcon />
<AlertTitle>
{restored} upload{restored === 1 ? "" : "s"} restored
</AlertTitle>
<AlertDescription>
Press resume to continue from the last finished part.
</AlertDescription>
</Alert>
) : null}
<UploadDropzone>
<UploadDropzoneTitle>
Drop a large file, then reload the page
</UploadDropzoneTitle>
<UploadDropzoneDescription>
The file and its finished parts are kept in IndexedDB
</UploadDropzoneDescription>
</UploadDropzone>
<UploadQueue />
</Upload>
)
}
Pause and resume
Pausing aborts the in-flight request. What happens on resume depends on the adapter:
| Adapter | Resumable | On resume |
|---|---|---|
s3Adapter (multipart) | ✓ | Continues from the last finished part |
multipartAdapter | ✓ | Continues from the last finished part |
tusAdapter | ✓ | Asks the server for the offset and continues |
s3Adapter (single PUT), presignedAdapter, httpAdapter | ✗ | - |
UploadPause only appears for resumable uploads (uploader.canPause(item)), so users
are never offered a pause that would silently restart. Queued items can always be paused.
Surviving reloads
Pass a persistence store. Queued and in-progress uploads, including the file bytes, are written to IndexedDB along with each adapter's resume state.
const [persistence] = React.useState(() => createIndexedDBPersistence())
const uploader = useUploader({ adapter, persistence, restore: true })After a reload, restore brings them back as paused with pauseReason: "restored".
Call uploader.resume() (or let users press resume) to continue.
Good to know
- Storage quotas. Browsers allow IndexedDB to use a large share of free disk space,
but may evict it under pressure. Call
requestPersistentStorage()to ask for durable storage. - Multiple tabs. When Web Locks are available, each running upload holds a lock, and
restore()skips uploads another tab is already running. - Expiry. Records older than 7 days are dropped (
maxAge). - Server state. Incomplete S3 multipart uploads are billed until aborted. Add a bucket lifecycle rule to clean up abandoned ones.
Persistence for other stores
Implement the three-method interface to persist elsewhere:
interface UploadPersistence {
load(): Promise<PersistedUpload[]>
save(record: PersistedUpload): Promise<void>
remove(id: string): Promise<void>
}