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:

PropertyPurpose
status"idle", "preparing", "uploading", "completing", "success", or "error"
progressAggregate progress from 0 to 100
progressFractionAggregate progress from 0 to 1
filesPer-file progress state
resultSuccessful upload completion result
errorLatest upload error
acceptFile input accept value from the upload manifest
constraintsClient-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.