Use <Form> to render forms that integrate cleanly
with backend validation and reactolith's submission model. Per-field
error display is just useFormErrors(name) — no
wrapper component required.
Reactolith forms are server-rendered — every field's current value lives in the HTML the backend emits. How you pass that value depends on the prop shape of the component (or native element) you're rendering:
| Field | Current value goes in… | Example |
|---|---|---|
Text-like input (text, email, number, …) |
The value attribute |
<input name="email" value="ada@example.com"> |
Native <textarea> |
Element children | <textarea name="bio">Hello</textarea> |
| Custom textarea component | Component-defined prop (often value) |
<ui-textarea name="bio" value="Hello"> |
Boolean (checkbox, switch) |
json-checked — not checked="true" |
<ui-checkbox name="newsletter" json-checked="true"> |
| Select / dropdown | Component prop (e.g. json-value) or per-option json-selected |
<ui-select name="country" json-value='"DE"'> |
| Radio group | Group-level json-value holding the active option |
<ui-radio-group name="plan" json-value='"pro"'> |
The boolean row is the easy one to get wrong: writing
checked="true" sends the literal string "true"
to the React prop, which is truthy and not a boolean. Always
use the json- prefix for non-string values — see the
encoding rules for the full table.
Render the form (and any backend errors) on the server, then ship
them to the frontend via json-errors:
After submission reactolith re-renders the new HTML — including new errors — without losing component state.
The morph-based render path is what makes server-driven forms feel responsive. The full loop:
json-errors payload.In practice that means your template is symmetric — the same markup handles the initial render and every subsequent re-render:
On the very first GET, form is empty and
errors is undefined, so the form renders blank. After a
failed POST the controller re-renders the same template with
form populated from the submitted request and
errors filled in — no separate "error template"
and no client-side state to reconcile.
:user-invalid
When errors include a name that matches an input,
<Form> calls setCustomValidity() on
that input. The browser then matches :user-invalid as
soon as the user interacts with the field, so styling invalid fields
is just CSS:
The first time the user edits a field, its custom validity is
cleared automatically and the field's entries disappear from
useFormErrors(name) — so any inline message
you render conditionally on that hook vanishes as the user
types. No React touched-state machinery needed. The cleared
set resets the moment a fresh errors payload
arrives from the server.
useFormSubmitting() returns true while the
form has been submitted but the next navigation has not yet finished.
Use it for spinners, disabled buttons, etc. The state is
per-<Form>, so multiple forms on a page can each
track their own.
useFormSubmitting() vs React 19's useFormStatus()
React 19 ships a built-in
useFormStatus()
hook in react-dom. Because reactolith requires React 19
as a peer dependency, that hook is always available alongside ours.
They look interchangeable but they track different
submissions and do not overlap today:
| Hook | Tracks | Use when |
|---|---|---|
useFormSubmitting() (reactolith) |
Submissions that bubble through reactolith's
Router — i.e. plain
<Form action="…" method="POST"> with no
React action prop, where the response HTML re-hydrates the
page.
|
You're using reactolith's HTML-driven submission model (the default for this library). |
useFormStatus() (React 19) |
Submissions kicked off by a React
form action
— i.e. <form action={fn}> where
fn is a function (server action or client
function), not a URL string.
|
You're calling a React function as the action and want React's built-in pending state in a child component. |
In a reactolith app the action is normally a URL, so
useFormStatus() would always report
{ pending: false }. Reach for
useFormSubmitting() instead. If you mix in a React 19
form action somewhere (e.g. a small client-side mutation that
doesn't navigate), useFormStatus() is the right tool
for that subtree — the two hooks are complementary, not
competitors.
| Hook | Returns |
|---|---|
useFormSubmitting() | boolean — whether the surrounding form is busy. |
useFormErrors(name?) | Errors for a single field (matched by name or id), or every error in the form when called without an argument. |
<Form> accepts an optional onSubmit
handler. Calling event.preventDefault() inside it stops
the request entirely (reactolith's Router will not pick it up
either), which is useful for client-side validation.