Skip to content

Forms

λ Router can intercept form submissions via the Navigation API. When a form with method="POST" submits to a URL that matches a route with a .formHandler(), the handler receives the FormData and the navigation event instead of rendering the route component.

Use the .formHandler() method on the route builder:

import { createRouter } from "@studiolambda/router/react";
const router = createRouter(function (route) {
route("/contact")
.formHandler(async function (formData, event) {
const name = formData.get("name");
const email = formData.get("email");
const message = formData.get("message");
await fetch("/api/contact", {
method: "POST",
body: JSON.stringify({ name, email, message }),
headers: { "Content-Type": "application/json" },
});
})
.render(ContactPage);
});
type FormHandler = (formData: FormData, event: NavigateEvent) => void | Promise<void>;
ParameterTypeDescription
formDataFormDataThe form data from the submission.
eventNavigateEventThe raw Navigation API event, providing access to destination URL, navigation type, etc.

When a <form method="POST"> submits and the Navigation API fires a navigate event, the Router checks:

  1. Does the destination URL match a registered route?
  2. Does that route have a formHandler?

If both are true, the form handler is called directly instead of going through the normal navigation lifecycle (prefetch → render). The form handler receives the FormData extracted from the navigation event and can perform server submissions, mutations, or any async operation.

function LoginPage() {
return (
<form method="POST" action="/login">
<label>
Email
<input type="email" name="email" required />
</label>
<label>
Password
<input type="password" name="password" required />
</label>
<button type="submit">Sign in</button>
</form>
);
}
const router = createRouter(function (route) {
route("/login")
.formHandler(async function (formData) {
const email = formData.get("email") as string;
const password = formData.get("password") as string;
const response = await fetch("/api/auth/login", {
method: "POST",
body: JSON.stringify({ email, password }),
headers: { "Content-Type": "application/json" },
});
if (!response.ok) {
throw new Error("Login failed");
}
// Navigate to dashboard after successful login.
navigation.navigate("/dashboard");
})
.render(LoginPage);
});

Form handlers compose with other route configuration:

route("/settings/profile")
.middleware([AuthGuard])
.formHandler(async function (formData) {
await updateProfile(Object.fromEntries(formData));
})
.render(ProfileSettings);

The middleware applies when rendering the page (GET navigation), and the form handler applies when submitting the form (POST navigation). This keeps your read and write logic cleanly separated on the same route.