The upload extension for form-atoms.
npm install @form-atoms/upload-atom
- 🧩 formAtom integrated: Your form submit will wait, while the upload is in progress.
- 🎮 File Input: Ready-to-use file input component.
▶️ Manual or Automatic upload: Start upload on file selection or manually.- ⚛️ React 19: uses the
<Suspense>to manage the loading state. - 💥 ErrorBoundary: use the react-error-boundary to catch upload errors.
- ⏱ Progress tracking: Monitor the upload progress with a progress bar.
import { fromAtom, useForm } from "form-atoms";
import {
uploadAtom,
FileInput,
FileUpload,
type UploadAtom,
} from "@form-atoms/upload-atom";
import { ErrorBoundary } from "react-error-boundary";
import { fetchUploadUrl, deliveryUrl, postFile } from "@/lib/cloudflare";
// 1. define your upload atom using some file service
export const cloudflareUploadAtom = uploadAtom(async (file) => {
const { id, uploadUrl } = await fetchUploadUrl();
try {
await postFile(uploadUrl, file);
return id; // the form field value
} catch {
// Throw string reason for the failure.
throw "Failed to upload.";
}
});
// 2. Use the uploadAtom inside a form as a regular fieldAtom:
const userForm = formAtom({
avatar: cloudflareUploadAtom(),
});
const Avatar = ({ atom }: { atom: UploadAtom<string> }) => {
// Read the upload result like a regular fieldValue:
const cfId = useFieldValue(url);
return (
<img
width={100}
height={100}
style={{ marginRight: 20 }}
src={deliveryUrl(cfId)}
/>
);
};
// 3. Render the FileUpload with suspense fallback, and error boundary:
export function Form() {
const { fieldAtoms, submit } = useForm(userForm);
return (
<form onSubmit={submit(console.log)}>
<ErrorBoundary
fallback={
<p>
Failed to upload. Use the <code>useFieldErrors()</code> hook to
display the reason thrown from your <code>upload</code> action.
</p>
}
>
<FileUpload
atom={fieldAtoms.avatar}
fallback={
<p>
Please wait... <progress />
</p>
}
>
{({ isIdle, isSuccess }) => (
<div>
{isIdle ? (
<>Please choose a file.</>
) : isSuccess ? (
<p>
<Avatar atom={fields.avatar} />
<ins>Done. </ins>
</p>
) : (
<></>
)}
</div>
)}
</FileUpload>
</ErrorBoundary>
<FileInput atom={fieldAtoms.avatar} />
<button type="submit">Submit</button>
</form>
);
}import { Progress } from "@form-atoms/upload-atom";
// You can use the setProgress callback to update the upload progress (if available):
export const progressAtom = uploadAtom(async (file, { setProgress }) => {
return myTrackUpload(file, { onProgress: setProgress });
});
export function Form() {
return (
<FileUpload
atom={progressAtom}
fallback={
<Progress atom={progressAtom}>
{({ progress }) => (
<>
{/* Render the value: */}
<p>please wait...</p>
<progress value={progress} max={1} />
</>
)}
</Progress>
}
>
{({ isIdle, isSuccess }) => null}
</FileUpload>
);
}See Storybook docs for more.