File uploader: Uploads
Pass the file uploader an upload function and it uploads: each file is queued, shows its progress, can be cancelled while it runs, and can be retried after it fails. The function receives the file and { signal, onProgress }, and returns the URL to keep.
Stored: nothing yet.
import { useState } from "react";
import { FileUploader, type UploadFn } from "slotsmith/file-uploader";
import { fakeUpload } from "../shared/fake-upload";
export default function Uploads() {
const [fail, setFail] = useState(false);
const [urls, setUrls] = useState<string[]>([]);
// Any async function will do: fetch, axios, a presigned PUT. Pass `signal` on
// so Cancel stops the request, and report progress as a percentage.
const upload: UploadFn = (file, { signal, onProgress }) => fakeUpload(file, { signal, onProgress }, { fail });
return (
<div style={{ display: "grid", gap: "0.75rem" }}>
<label style={{ display: "flex", gap: "0.5rem", alignItems: "center" }}>
<input type="checkbox" checked={fail} onChange={(event) => setFail(event.target.checked)} />
Make the next uploads fail
</label>
<FileUploader
multiple
maxFiles={5}
maxSize={10 * 1024 * 1024}
// Two uploads run at once; the rest wait their turn.
concurrency={2}
upload={upload}
// The URLs of the finished uploads: what a form field would store.
onUrlsChange={setUrls}
/>
<p>{urls.length === 0 ? "Stored: nothing yet." : `Stored: ${urls.join(", ")}`}</p>
</div>
);
}/** What the uploader hands an `upload` function, as far as this stand-in reads it. */
export interface FakeUploadContext {
signal: AbortSignal;
onProgress(percent: number): void;
}
/** Five steps of 200 ms: the whole upload takes one second. */
const STEPS = 5;
const STEP_MS = 200;
/** Waits `ms`, and rejects with an `AbortError` as soon as the signal aborts. */
function wait(ms: number, signal: AbortSignal): Promise<void> {
return new Promise((resolve, reject) => {
let timer: ReturnType<typeof setTimeout> | undefined;
const abort = () => {
clearTimeout(timer);
reject(new DOMException("The upload was cancelled.", "AbortError"));
};
if (signal.aborted) return abort();
timer = setTimeout(() => {
signal.removeEventListener("abort", abort);
resolve();
}, ms);
signal.addEventListener("abort", abort, { once: true });
});
}
/**
* A stand-in for an upload endpoint. Nothing leaves the browser: it reports
* progress in five steps over one second, stops as soon as `signal` aborts,
* and resolves with `/samples/uploaded/<name>`. With `fail`, it fails at 60%
* the way a server error would, so the retry path can be tried.
*/
export async function fakeUpload(
file: File,
{ signal, onProgress }: FakeUploadContext,
{ fail = false }: { fail?: boolean } = {},
): Promise<{ url: string }> {
for (let step = 1; step <= STEPS; step += 1) {
await wait(STEP_MS, signal);
if (fail && step === 3) throw new Error("The server answered 503. Try again.");
onProgress((step / STEPS) * 100);
}
return { url: `/samples/uploaded/${encodeURIComponent(file.name)}` };
}Drop three files: two upload at once and the third waits. Press Cancel on one while it runs. Then tick the box, add another file, and retry it once it fails. The uploads are simulated in the page; nothing leaves your browser.
The upload function
upload is called once per file with the file and a context:
signalis anAbortSignal. It aborts when the item is cancelled or removed, or the component unmounts. Pass it tofetchor axios so the request stops too.onProgress(percent)takes a number from 0 to 100. Values are rounded and clamped.itemis the item being sent, for an id the server needs.
It returns a promise of a URL string, an object { url, data }, or nothing. The URL goes into the item and into onUrlsChange; data is kept on the item for ids and metadata the server sends back. A rejected promise marks the item failed, and the error’s message is shown in it. The demo’s fakeUpload, in its fake-upload.ts tab, has the same shape.
The queue
Uploads start as soon as files are added. concurrency sets how many run at once (3 by default; the demo uses 2), and the rest wait as ready. With autoUpload set to false, nothing starts until you call uploadAll() from useFileUploaderContext().
Cancel, retry and remove
Each item’s actions follow its status:
- Uploading: Cancel aborts the signal and puts the item back to
ready. A cancelled item has no Retry button;uploadAll()sends it again. Remove aborts the upload and drops the item. - Failed: the error replaces the size, and Retry sends the file again from zero.
- Any status: Remove drops the item and releases its preview.
onUploaded and onUploadError are called per file, for a toast or a log.