Form
Submit to a registered route and navigate without reloading the document.
Compatibility
Import from @kasperrt/smolter/react @kasperrt/smolter/preact
Signature
function Form(options: InternalFormProps): ReactNode;Parameters
action defaults to the current URL. method defaults to post; get, post, put, patch, and delete are supported. Standard form attributes and onSubmit are forwarded.
Returns
A form element.
Usage
Import this API from one of the entry points listed above. Keep server and build helpers in their dedicated subpaths.
<Form action="/tasks/search" method="get">
<input name="q" />
<button>Search</button>
</Form>;Behavior and edge cases
GET serializes form values into a new query string and calls the loader through navigation. Mutations call the action, then revalidate. Submitter formaction and formmethod override form defaults. GET files contribute their filenames. Use fetcher.Form to avoid navigation. Native markup uses GET or POST; PATCH/PUT/DELETE require the JavaScript handler.
Save without leaving the page
The action returns validation data or a successful result. The fetcher is independent from the page loader, so display action feedback from fetcher.data and the refreshed saved value from useLoaderData. Keep a stable button label and show pending text separately to avoid moving controls.
// routes/settings/view.tsx
import { useFetcher, useLoaderData } from "@kasperrt/smolter/react";
import type { clientAction } from "./action";
import type { clientLoader } from "./loader";
export default function Settings() {
const data = useLoaderData<typeof clientLoader>();
const fetcher = useFetcher<typeof clientAction>();
const busy = fetcher.state !== "idle";
return (
<section>
<h1>Settings</h1>
<p>Saved name: {data?.displayName}</p>
<fetcher.Form action="/settings" method="post" aria-busy={busy}>
<label htmlFor="displayName">Display name</label>
<input
id="displayName"
name="displayName"
defaultValue={data?.displayName}
required
minLength={2}
/>
<button disabled={busy}>Save name</button>
<p role="status">{busy && "Saving…"}</p>
{fetcher.data?.ok === false && <p role="alert">{fetcher.data.error}</p>}
{fetcher.data?.ok === true && <p role="status">Saved.</p>}
{fetcher.error && <p role="alert">{fetcher.error.message}</p>}
</fetcher.Form>
</section>
);
}Validate in the handler
required, minLength, and type="email" provide browser feedback, but handlers must validate submitted values too. Return a discriminated result such as { ok: false, error } for a correctable input problem. Connect field messages with aria-describedby and render async feedback in a status or alert region. Unexpected request failures belong in fetcher.error or a route error boundary.
// routes/settings/action.ts
import type { DataArgs } from "@kasperrt/smolter/react";
export async function clientAction({ request }: DataArgs) {
const form = await request.formData();
const displayName = String(form.get("displayName") ?? "").trim();
if (displayName.length < 2) {
return { ok: false as const, error: "Use at least two characters." };
}
localStorage.setItem("displayName", displayName);
return { ok: true as const, displayName };
}Target another route and method
A form can call a route other than the currently rendered page. Use a concrete URL for dynamic routes, such as /tasks/42. The pattern /tasks/:id is registered in the table; it is not the URL to submit. Each form below calls the same task route with a different method.
<fetcher.Form action="/tasks/42" method="patch">
<label>Title <input name="title" required /></label>
<button>Rename</button>
</fetcher.Form>
<fetcher.Form action="/tasks/42" method="delete">
<button>Delete task</button>
</fetcher.Form>Submit from application code
Use fetcher.submit for interactions that already have structured values. Pass a FormData instance and an explicit route URL. This invokes the same action as fetcher.Form. Calling submit does not run native input constraint validation; validate in the handler.
const body = new FormData();
body.set("title", "Review documentation");
await fetcher.submit(body, {
action: "/tasks/42",
method: "PATCH",
});Intercept submission deliberately
onSubmit runs before smolter intercepts the form. Calling event.preventDefault() stops smolter as well. Keep normal data work in the action instead of duplicating it in onSubmit. External form destinations are left to the browser.
Files and encoding
Mutation forms create FormData, which can contain File values. Read them with request.formData() in the action. The server transport forwards multipart FormData; it does not implement upload storage, size limits, or streaming policies. GET serializes a selected file as its filename. Setting encType does not turn the intercepted mutation into a JSON request.
SSR and JavaScript requirements
Server rendering outputs a real form element, but a complete no-JavaScript submission flow requires an HTTP application that accepts the native form URL and returns HTML or a redirect. handleRequest returns data, not a complete HTML response. PATCH, PUT, and DELETE rely on the JavaScript handler; the underlying HTML form uses POST. Do not assume SSR alone provides progressive enhancement.