Form submission
useSubmitForm connects an existing @mongez/react-form form to the app’s configured HTTP client. Install its optional peers and import it from the form subpath:
import { Form } from "@mongez/react-form";import { useSubmitForm } from "@warlock.js/web/form";
export default function CreateOrder() { const createOrder = useSubmitForm({ route: "orders.create", params: { customerId: "42" }, onSuccess: (result) => console.log(result.data), });
return <Form onSubmit={createOrder.submit}>{/* controls */}</Form>;}It needs @mongez/http:^3.5.0 and @mongez/react-form:^4.0.0. By default it uses the configured http singleton, preserving its base URL, interceptors, authentication, and error handling. Pass client when a different configured Http instance is required.
Pass exactly one of route or path. A named route uses its server-declared method; a direct path defaults to POST and accepts method. Use params for path segments and query for URL parameters. GET and HEAD send form values as query data; other requests send native FormData.
Named API routes are captured in the same generated declaration file as page
routes. With .warlock/typings/web-routes.d.ts included in the app TypeScript
project, route and params are checked against the registered API route.
The browser receives only each API route’s name, path, and method; it never
receives handlers, schemas, or server policy. An API route registered with
method "all" does not prevent boot, but cannot be submitted by name: choose
an explicit path and method instead.
The hook returns submit, data, error, response, isLoading, formErrors, reset, and cancel. Data, error, and response begin as null; loading begins as false and form errors as an empty array. Lifecycle callbacks receive the full HttpResult. beforeSubmit may return false to cancel before a request. Duplicate calls share the pending promise; cancellation and unmount prevent stale state updates. reset() clears results and errors assigned by the hook without clearing form values or cancelling an active request.
Validation field mapping is enabled by default. Matching fields receive server errors, while unknown field names and general errors are collected in formErrors. Disable it with mapFieldErrors: false, or supply a mapper returning field-message pairs.
Named route metadata deliberately contains only name, path, and method (plus the type-only response map described below). A route registered with all has no safe browser verb; use a direct path with an explicit method.
Which named routes a page can use
Section titled “Which named routes a page can use”route and apiHref() resolve a name against the route table of the page being
shown. Each page document carries the named routes its client code reaches (see
Page setup files).
A name the page does not carry throws. In development the error names the route
and the declaration that fixes it, and in production it is a short error with no
fallback to the full table. Use a path and method for a call the build cannot see,
or declare the name with config.apiRoutes.
The table is replaced on every client navigation, so the new page’s routes are
available as soon as its tree renders. Authorization is not decided by the table:
a route the page can see still answers 401 or 403 from the server.
Control the route table
Section titled “Control the route table”Besides clientVisible: false, three rules keep routes out of a table, and
web.namedApiRoutes adds your own. The rules only remove routes: a page still
carries just the routes its client code reaches.
| Rule | Applies to |
|---|---|
Routes under privatePrefixes | every viewer and every site |
Routes declared for other sites (sites route or group option, core) | a page served by a site; a route without sites is shared |
| Routes with an auth guard in their middleware chain, group middleware included | a guest viewer |
import type { WebConfigurations } from "@warlock.js/web";
const web: WebConfigurations = { namedApiRoutes: { privatePrefixes: ["/api/admin/", "/api/webhooks/"], // Optional: only these names (or `prefix.*` patterns), or a filter function. include: ["orders.*", "cart.add"], },};
export default web;namedApiRoutes takes one of these forms:
falseembeds no table at all.apiHref()anduseSubmitForm({ route })throw, and in development the error names this option. Pass an explicitpathandmethod, which always work. Choose this when the app never calls routes by name.- An allowlist (
["orders.*", "cart.add"]) or a filter function, shorthand for{ include }. { include, privatePrefixes }.
A filter receives the route (name, path, method, sites, guarded) and the context
(site, the serving site’s key or undefined, and guest):
namedApiRoutes: (route, { site, guest }) => !(guest && route.name.startsWith("account.")),Private prefixes match whole path segments: /api/admin covers /api/admin/users, not
/api/administrators.
A viewer is a guest only when web.session is configured and resolves no user. Without
web.session the viewer is unknown (the app may sign in with tokens in the browser), so
guarded routes stay in the table. A guest’s page
is cacheable and carries the guest table; a signed-in viewer’s render is never cached and
carries the guarded routes its code reaches. A login does not re-render the page a visitor
already holds, so after signing in without a navigation the page still has the guest table:
navigate or reload to receive the full one, or call the guarded endpoint with an explicit
path and method. Authorization never depends on the table. A route the page can see still
answers 401 or 403 from the server’s guards, and the table only limits what the browser
is told.
Typed API responses
Section titled “Typed API responses”Since 5.27, a route whose handler declares responseSchema (see core’s Declare response types) gets a response map in its generated ApiRouteRegistry entry, and useSubmitForm({ route }) types data, response and onSuccess from the route’s declared 2xx body. There is no generic argument and no cast.
login.responseSchema = { 200: { body: { user: UserResource, token: "string" } }, 400: { body: { error: "string" } },};import { Form } from "@mongez/react-form";import { useSubmitForm } from "@warlock.js/web/form";
export default function Login() { const login = useSubmitForm({ route: "auth.login", onSuccess: async ({ data }) => { if (data) { console.log(data.user.name, data.token); // typed from the 200 body } }, });
return <Form onSubmit={login.submit}>{/* controls */}</Form>;}login.data has the same type, or null until a request succeeds. With several declared 2xx statuses the body is the union of them. Read a declared body anywhere with the public types from @warlock.js/web:
| Type | What it is |
|---|---|
ApiResponse<Name, Status = 200> | The declared body for that status, or unknown when the route or the status is not declared (never any). |
ApiResponses<Name> | The whole status-to-body map. Empty for an undeclared route or a runtimeRoute name. |
ApiSuccessResponse<Name> | The union of the declared 2xx bodies, or unknown when there is none. |
import type { ApiResponse } from "@warlock.js/web";
type LoginError = ApiResponse<"auth.login", 400>; // { error: string }- A route without
responseSchemakeepsunknown, and an app with noresponseSchemaat all generates a byte-identical declaration file. - A direct
pathtarget, and an explicituseSubmitForm<Schema, Data>call, are unchanged: you supplyDatayourself. - Each resource in a schema is mapped back to its
*.resource.ts(x)export. A resource defined elsewhere is typedunknown, and core warns with the route and field. - The declaration file holds types only; the
responsemap adds nothing to what the browser receives at runtime.
Page actions instead
Section titled “Page actions instead”useSubmitForm posts to an API route and leaves the result to your callbacks. When the form belongs to a page and the outcome should re-render it or redirect (and work without JavaScript), export an action from the page and use <Form>; see Page actions.
Prefetch cache (5.21)
Section titled “Prefetch cache (5.21)”The client prefetch cache is cleared on refresh, on locale change and after form submits. clearPrefetchCache() is exported from @warlock.js/web if you need to clear it yourself after a mutation made outside a form.