Forms

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.

import { Form, useFormErrors, useFormSubmitting } from "reactolith"; function MyForm({ errors }) { return ( <Form action="/users" method="POST" errors={errors}> <label> Email <input name="email" /> </label> <FieldError name="email" /> <label> Password <input name="password" type="password" /> </label> <FieldError name="password" /> <SubmitButton /> </Form> ); } function FieldError({ name }) { const errors = useFormErrors(name); if (!errors.length) return null; return <span role="alert">{errors[0].message}</span>; } function SubmitButton() { const submitting = useFormSubmitting(); return ( <button type="submit" disabled={submitting}> {submitting ? "…" : "Save"} </button> ); }

Populating fields server-side

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.

Backend validation errors

Render the form (and any backend errors) on the server, then ship them to the frontend via json-errors:

<my-form action="/users" method="POST" json-errors='[{"name":"email","message":"Already taken"}]' > <ui-input name="email" /> </my-form>

After submission reactolith re-renders the new HTML — including new errors — without losing component state.

Round-trip after a validation error

The morph-based render path is what makes server-driven forms feel responsive. The full loop:

  1. Server renders the form — each field's current value lives in the HTML.
  2. User edits a field. The DOM input keeps the typed value; no React re-render is involved.
  3. Submit. Reactolith intercepts and POSTs.
  4. Server validates, fails, re-renders the same template with the user's submitted values and a json-errors payload.
  5. Reactolith morphs the new HTML over the live tree. The focused field stays focused, its cursor position survives, sibling state (open dialogs, scroll, …) is untouched.

In practice that means your template is symmetric — the same markup handles the initial render and every subsequent re-render:

{# users/new.html.twig #} <my-form action="/users" method="POST" {% if errors %} json-errors='{{ errors|json_encode|e('html_attr') }}'{% endif %} > <ui-input name="email" value="{{ form.email|default('') }}"> <ui-checkbox name="newsletter" {% if form.newsletter %}json-checked="true"{% endif %}> <ui-button type="submit">Sign up</ui-button> </my-form>

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.

Native validity & :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:

input:user-invalid { border-color: red; }

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.

Submitting state

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.

Hooks

HookReturns
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.