Skip to content
Forms
FrameworkForms
On this page

Forms

Submit to a route handler and render pending state, validation errors, and results beside the fields.

A complete form

This route accepts a name and returns a greeting. It handles both native multipart submissions and enhanced object fields:

src/routes/greet.jsx
import { Form } from "goribu";

// Show the current submission's state beside the field.
export default function GreetPage() {
  return (
    <Form method="post">
      {({ pending, errors, data }) => (
        <>
          <label htmlFor="name">Name</label>
          <input id="name" name="name" aria-describedby="name-error" />
          <p id="name-error">{errors?.name}</p>
          <button disabled={pending}>
            {pending ? "Sending…" : "Greet me"}
          </button>
          <p role="status">{data?.greeting}</p>
        </>
      )}
    </Form>
  );
}

// Validate a native or enhanced submission before returning a result.
export async function POST(req, res) {
  const body = await req.body();
  const name = body instanceof FormData
    ? body.get("name")
    : typeof body === "object" ? body.name : undefined;
  if (typeof name !== "string" || !name.trim()) {
    return res.invalid({ name: "Enter your name." });
  }
  return res.json({ greeting: `Hello, ${name.trim()}!` });
}

method is required. action defaults to the current URL at submission time. Use action="/another-route" to send the request elsewhere.

Choose the result

Handler resultEnhanced submission
res.json(data)Stay on the page and expose data.
res.invalid(errors)Show errors, mark matching controls, and focus the first invalid field.
res.redirect(path)Navigate to the destination without repeating the mutation.
res.render(props)Render this route in place with fresh props and metadata.

After saving, return res.redirect("/posts") when the next screen is a list or detail page. Add replace to Form to replace the current history entry during enhanced navigation.

Use res.render() only when the action pathname matches the displayed page. For another route, redirect instead. Goribu uses the same-origin Referer to verify a same-page render; when it cannot verify that relationship, it recovers with a document GET rather than replaying the write.

Validation

Return an object whose keys match control names, or a string for a form-level error:

Inside a handler
return res.invalid({
  name: "Enter a name.",
  root: "The request could not be saved.",
});

The default status is 422. A string becomes errors.root. Goribu adds aria-invalid to matching controls and focuses the first invalid control in DOM order. You render the messages and connect them using aria-describedby.

Always validate input and authorization in the handler. Browser constraints are a useful first check, not a replacement for server validation.

GET forms

Use GET for searches and filters:

Search form
import { Form } from "goribu";

// Put the search term in the URL so results can be shared.
export default function SearchForm() {
  return (
    <Form method="get" action="/search" replace>
      <label>Search <input name="q" /></label>
      <button>Search</button>
    </Form>
  );
}

Read the value in the destination's GET handler with req.query.get("q"). Submitted fields replace the action's existing query string. Files are omitted. GET navigates; it does not produce in-place data or validation state.

Methods and files

Form supports get, post, put, patch, and delete, in upper or lower case. Extended methods send the real HTTP method to the matching route export; there is no hidden POST method override.

Goribu selects encoding. Enhanced mutations use URL-encoded fields unless an enabled, named file input requires multipart data. Native GET and POST forms render with multipart encoding; a POST body is therefore FormData before hydration. Do not set encType yourself.

Read an upload with const body = await req.body() and body.get("file") after checking body instanceof FormData. Validate file size and type on the server.

Pending state and completion

Render-prop children receive { pending, errors, data }. Pending becomes true when submission is accepted. Duplicate submissions from that form are blocked until completion, and old errors clear when a new submission starts.

reset clears controls after successful JSON or same-page render results, not validation errors or redirects. onSubmit can cancel with preventDefault(). onResult runs after local state and an in-place render commit, but before redirect navigation; inspect result.ok before assuming success.

A forwarded ref exposes the form element. Unmounting aborts an active request, and a newer navigation invalidates old form results.

Before hydration

GET and POST work as native forms. Redirects navigate normally, renders return HTML, and JSON or validation results display as full-page JSON.

PUT, PATCH, and DELETE require hydration. Their controls remain inactive without JavaScript. Cross-origin actions and non-default targets are native-only for GET/POST; extended methods require a same-origin action and the current browsing context.