React uploads
@beignet/react-uploads adds React hook state on top of the typed browser
upload client from @beignet/core/uploads/client. It does not define another
upload protocol. The core client still owns prepare, direct upload, server
fallback, completion, typed route names, metadata, and errors.
Use it when a component needs upload progress, pending state, errors, reset, or abort behavior.
Install
bun add @beignet/react-uploads
Create the adapter
Create the upload client once, then wrap it for React:
// client/uploads.ts
import { createUploadClient } from "@beignet/core/uploads/client";
import { createReactUploads } from "@beignet/react-uploads";
import type { postUploads } from "@/features/posts/uploads";
type AppUploads = typeof postUploads;
export const uploads = createUploadClient<AppUploads>({
baseUrl: "/api/uploads",
headers: async () => ({
"x-user-id": "alice",
"x-tenant-id": "tenant_example",
}),
});
export const reactUploads = createReactUploads({
uploads,
});
Import the upload registry as a type so browser code does not bundle server-only upload hooks.
Use an upload hook
Bind the hook to an upload name:
"use client";
import { reactUploads } from "@/client/uploads";
export function AttachmentInput({ postSlug }: { postSlug: string }) {
const attachment = reactUploads.useUpload("posts.attachment");
return (
<div>
<input
type="file"
accept={attachment.accept}
multiple
disabled={attachment.isUploading}
onChange={(event) => {
const files = Array.from(event.currentTarget.files ?? []);
if (files.length === 0) return;
attachment.upload({
metadata: { postSlug },
files,
});
}}
/>
{attachment.isUploading && <p>{attachment.progress}%</p>}
{attachment.isError && <p>Upload failed</p>}
</div>
);
}
The hook returns:
| Property | Purpose |
|---|---|
status | "idle", "preparing", "uploading", "completing", "success", or "error" |
progress | Aggregate progress from 0 to 100 |
progressFraction | Aggregate progress from 0 to 1 |
files | Per-file progress state |
result | Successful upload completion result |
error | Latest upload error |
accept | File input accept value from the upload manifest |
constraints | Client-safe file constraints from the upload manifest |
upload(...) | Start an upload with an explicit files array |
uploadFile(...) | Start an upload with one file |
abort() | Abort the active upload request |
reset() | Clear local hook state |
Many-file ergonomics
Use useUploadMany(...) when the component already has a file array and you
want a file-first call signature:
const attachments = reactUploads.useUploadMany("posts.attachment");
await attachments.upload(files, {
metadata: { postSlug },
});
Success and error callbacks
Callbacks can be defined on the hook or per upload call. Use onSuccess to
invalidate React Query state or close a dialog after the upload creates app
records.
const attachment = reactUploads.useUpload("posts.attachment", {
onSuccess() {
queryClient.invalidateQueries({
queryKey: rq(getPost).key({ path: { slug: postSlug } }),
});
},
});
The adapter intentionally does not hide TanStack Query. Uploads are imperative side effects; cache invalidation should stay app-owned and explicit.