React
Use React bindings built on the framework-free client engine.
The React bindings are ergonomics over the framework-free client. Create the helpers once, export the hooks from a client-side module, and keep the rest of your components focused on upload UI.
Setup
"use client";
import { createUploadStuffClient } from "@upload-stuff/client";
import { createUploadStuffReactHelpers } from "@upload-stuff/react";
import type { FileRouter } from "./server/upload-router";
const client = createUploadStuffClient<FileRouter>({
baseURL: "https://api.example.com",
});
export const { useUploadStuff, useRouteConfig } = createUploadStuffReactHelpers(client);createUploadStuffReactHelpers(client) returns exactly { useRouteConfig, useUploadStuff }. The client carries your server router type, so both hooks know your route names, inputs, and completion payloads.
useUploadStuff
Use useUploadStuff(endpoint, options?) when a component owns an upload interaction. It returns startUpload, loading state, whole-run progress, abort, the resolved routeConfig, and an accept string you can pass straight to a file input.
function AvatarUpload() {
const { startUpload, isLoading, isUploading, progress, abort, accept } = useUploadStuff((r) => r.avatar, {
onClientUploadComplete: (res) => console.log(res),
onUploadError: (err) => console.error(err.message),
});
return (
<>
<input
type="file"
accept={accept}
disabled={isLoading || isUploading}
onChange={(event) => {
const files = Array.from(event.currentTarget.files ?? []);
startUpload(files, { albumId: "profile" }).catch(() => {});
}}
/>
<button type="button" disabled={!isUploading} onClick={abort}>
Cancel
</button>
<span>{progress}%</span>
</>
);
}startUpload takes (files, input?, options?), not an options.input object. That object shape belongs to the framework-free client. In React, pass route input as the second positional argument — and when the route declares an input schema, that argument is required:
void startUpload(files, { albumId: "profile" }, { headers: { "x-upload-source": "avatar-form" } });Per-call options are signal and headers. Hook-level options are the shared upload callbacks plus optional headers; when both levels set headers, the per-call headers win.
useRouteConfig
Use useRouteConfig(endpoint) when you need the route config without starting an upload:
function AvatarRules() {
const { data, error, isLoading, refetch } = useRouteConfig("avatar");
if (isLoading) return <p>Loading upload rules...</p>;
if (error) return <button onClick={refetch}>Retry</button>;
return <p>You can upload up to {data?.maxFileCount} file.</p>;
}useRouteConfig takes a route key, not the selector shorthand that useUploadStuff accepts. It returns { data, error, isLoading, refetch }. A failed refresh keeps serving the existing data; only the first load exposes error.
Endpoint switching
When a useUploadStuff binding switches endpoints, the visible upload state resets for the new endpoint. An upload that was already running is orphaned from the new binding: its callbacks can still run, but it cannot update the new binding's isUploading or progress. Route-config state also resets while the new endpoint's config loads.