Root

Holds the file list and hands it to every child through context. One required prop: upload.

import { Root } from "uplofile";

Usage

Every variant below is the same component. Switch tabs to see what each prop changes.

import { Root } from "uplofile";
export function Uploader({ children }: { children: React.ReactNode }) {
return (
<Root
upload={async (file) => {
const body = new FormData();
body.append("file", file);
const response = await fetch("/api/upload", { method: "POST", body });
if (!response.ok) throw new Error("Upload failed");
return response.json(); // { url, id?, meta?, previewUrl? }
}}
>
{children}
</Root>
);
}

Props

uploadrequired(file, signal, setProgress?) => Promise<UploadResult>
Your transport. Resolve with { url, id?, meta?, previewUrl? }; throw to mark the item errored and enable retry.
removeMode"optimistic" | "strict"
Optimistic drops the item at once and restores it if onRemove rejects. Strict waits for the promise.
"optimistic"
beforeUpload(items, state) => boolean | Array<{ uid, valid, meta?, id?, reason? }>
Gate or enrich files before transport. Async allowed. Return per-file verdicts to reject some and keep the rest; state carries prevItems, remaining, maxCount, and accept.
onRemove(item, signal) => Promise<unknown>
Server-side delete. Pair with removeMode. If it throws or rejects, the item stays (or is restored, in optimistic mode) at status: "done" with error set to the failure message; a new removal attempt clears it.
initialInitialItem[] | Promise<InitialItem[]>
Files already on your server. Each item needs uid, name, and url; id and meta are optional. If you pass a promise, you own its rejection — Root does not attach a .catch.
[]
onChange(items: UploadFileItem[]) => void | Promise<void>
Fires on every list change, where you can sync form state.
onLoadingChange(isLoading: boolean) => void
True while initial is still resolving.
acceptstring·multipleboolean·maxCountnumber
Passed straight to the file input. maxCount is also readable inside beforeUpload.
"image/*"
true
namestring·disabledboolean
The field name for HiddenInput, and a blanket interaction lock.
"image"
false

Ref API

A ref reaches the same methods from outside the context. It is how you make a whole page a drop target while children still read state.

PageDropTarget.tsx
import { useRef } from "react";
import { Preview, Root, type UplofileRootRef } from "uplofile";
const upload = async (file: File) => ({ url: URL.createObjectURL(file) });
export function PageDropTarget() {
const ref = useRef<UplofileRootRef>(null);
return (
<main
onDrop={(event) => ref.current?.onDrop?.(event)}
onDragOver={(event) => ref.current?.onDragOver?.(event)}
>
<Root ref={ref} upload={upload}>
<Preview />
</Root>
</main>
);
}
getItems()
read
setItems(next)
write
onDrop(e)
event
onDragOver(e)
event
openFileDialog()
action
actions
cancel · remove · retry
isLoading
boolean