File uploader: Virtual lists
For hundreds of files, pass virtual to the file uploader with the virtualizer hook as use (useVirtualizer from @tanstack/react-virtual): it renders only the rows in view inside a scroll area. VirtualFileUploader from slotsmith/virtual passes the hook for you. The queue, progress, retry and every slot work the same.
import type { UploadItem } from "slotsmith/file-uploader";
import { VirtualFileUploader } from "slotsmith/virtual";
import { fakeUpload } from "../shared/fake-upload";
// 300 scans already stored, built from their index so every render gets the same ones.
const scans: UploadItem[] = Array.from({ length: 300 }, (_, index) => {
const name = `scan-${String(index + 1).padStart(3, "0")}.pdf`;
return {
id: name,
name,
size: 40_000 + ((index * 7919) % 900_000),
type: "application/pdf",
status: "done",
progress: 100,
url: `/samples/uploaded/${name}`,
};
});
export default function Virtual() {
return (
<VirtualFileUploader
multiple
maxFiles={500}
defaultValue={scans}
upload={fakeUpload}
// A fallback row is 64.5px tall plus the 8px gap; the list scrolls inside 320px and renders only what is in view.
virtual={{ estimateSize: 72.5, maxHeight: 320 }}
/>
);
}/** 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)}` };
}The list holds 300 stored scans and scrolls inside 320px; only about a dozen rows are in the page at a time. Add more files and they upload at the end of the list.
Install
The hook comes from TanStack Virtual, an optional peer dependency that slotsmith never imports itself. Install it next to slotsmith:
npm i @tanstack/react-virtual
The virtual option
The virtual prop takes the hook as use (VirtualFileUploader passes it for you), and three numbers:
estimateSize: the height of one row in pixels, 56 by default. The rows are not measured, so set it to your row height plus the gap between rows: a fallback row is 64.5px and the gap 8px, so the demo uses 72.5.maxHeight: the height of the scroll area in pixels, 400 by default.overscan: how many rows to render beyond the visible area, 6 by default.
Rows out of view are replaced by two spacer <li> elements, hidden from assistive technology, that keep the scrollbar true to the whole list. The spacers never shrink and allow for the list’s row gap, so the fallback flex list scrolls through every row, and so does a replacement List laid out as a grid or a block.
Your own layout
The list on its own is FileUploaderVirtualList, also from slotsmith/virtual. Use it in place of FileUploader.List inside the compound parts, with the same options as props.