You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit e1453fe
Browse filesBrowse the repository at this point in the historyBrowse files
- Split "Loaders & Actions" into "Loaders & Props" and "Actions & Forms"
- Merge deferred props into loaders page
- Merge forms into actions page with named actions section
- Move layouts before loaders in sidebar
- Move directory structure before navigation in overview
- Add SVG layout nesting diagram
- Rename files: loaders.md, actions-and-forms.md
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: docs/guide/pages-routing/actions-and-forms.md
+110-4Lines changed: 110 additions & 4 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -2,11 +2,80 @@
2
2
outline: deep
3
3
---
4
4
5
-
# Forms
5
+
# Actions & Forms
6
6
7
-
`useForm` handles form submissions, loading state, and validation errors. It is **fully typed end-to-end** — the URL autocompletes to pages that have an `action`, `form.data` is typed to match the action's validator schema, and `form.errors` keys are constrained to the body's field names. No manual type annotations needed.
7
+
Actions handle mutations (POST, PUT, PATCH, DELETE) in companion `.server.ts` files. They use the same `defineHandler` API as [loaders](./loaders) and [server routes](../server-routing.md), with the same typed `c.env` bindings, `withValidator()` support, and Hono context methods.
8
8
9
-
## Usage
9
+
## Defining an Action
10
+
11
+
Export `action` from a `.server.ts` file. Use `withValidator()` with a [schema-derived validator](../database.md#schema-derived-validators) to validate the request body:
12
+
13
+
```ts
14
+
// pages/users/index.server.ts
15
+
import { defineHandler } from"void";
16
+
import { db } from"void/db";
17
+
import { users, insertUserSchema } from"@schema";
18
+
19
+
exportconst action =defineHandler.withValidator({
20
+
body: insertUserSchema,
21
+
})(async (c, { body }) => {
22
+
awaitdb.insert(users).values(body);
23
+
// No return → re-runs loader, page re-renders with fresh data
| Nothing (void) | Re-runs the loader, page re-renders with fresh props. |
30
+
|`c.redirect('/path')`| Navigates to another page. |
31
+
32
+
## Named Actions
33
+
34
+
When a page needs multiple mutations — for example, updating and deleting a user on the same page — export `actions` (plural) instead of `action`:
35
+
36
+
```ts
37
+
// pages/users/edit.server.ts
38
+
import { defineHandler } from"void";
39
+
import { db, eq } from"void/db";
40
+
import { users } from"@schema";
41
+
42
+
exportconst actions = {
43
+
update: defineHandler.withValidator({
44
+
body: updateUserSchema,
45
+
})(async (c, { body }) => {
46
+
awaitdb
47
+
.update(users)
48
+
.set(body)
49
+
.where(eq(users.id, c.req.param("id")));
50
+
}),
51
+
52
+
delete: defineHandler(async (c) => {
53
+
const { id } =awaitc.req.json<{ id:string }>();
54
+
awaitdb.delete(users).where(eq(users.id, id));
55
+
}),
56
+
};
57
+
```
58
+
59
+
Named actions are dispatched via a `?actionName` suffix on the URL (e.g. `/users/edit?update`). The client primitives `useForm` and `action()` handle this automatically.
60
+
61
+
`export const action` (singular) still works for pages that only need one mutation. Named actions are opt-in. Exporting both `action` and `actions` from the same file is an error.
62
+
63
+
You can also define an `actions.default` key for the action that runs when no name is specified (i.e. a bare POST to the page URL):
64
+
65
+
```ts
66
+
exportconst actions = {
67
+
default: defineHandler(async (c) => {
68
+
// runs on POST /users/edit (no ?suffix)
69
+
}),
70
+
delete: defineHandler(async (c) => {
71
+
// runs on POST /users/edit?delete
72
+
}),
73
+
};
74
+
```
75
+
76
+
## `useForm`
77
+
78
+
`useForm` handles form submissions, loading state, and validation errors. It is **fully typed end-to-end** — the URL autocompletes to pages that have an action, `form.data` is typed to match the action's validator schema, and `form.errors` keys are constrained to the body's field names. No manual type annotations needed.
10
79
11
80
First, define the action in your server handler:
12
81
@@ -133,7 +202,7 @@ export default function CreateUser() {
133
202
134
203
The types are inferred from your action's `withValidator()` schema in the companion `.server.ts` file. If no validator is defined, the body type falls back to `Record<string, unknown>` — you still get URL autocomplete either way.
When a page exports named actions, append `?actionName` to the URL:
230
+
231
+
```ts
232
+
const form =useForm("/users/:id?update", { name: "" }, { params: { id } });
233
+
form.put(); // submits to /users/42?update
234
+
```
235
+
236
+
The URL, body fields, and error keys are all typed per-action — each named action gets its own validator schema. See [Type Safety — Action → useForm](../type-safety#action-useform) for the full typing story.
237
+
238
+
## `action()` Helper
239
+
240
+
For one-shot mutations that don't need form state (dirty tracking, reset, field-level errors), use `action()` instead of `useForm`. It sends a request to a page action and triggers an Inertia page update, just like `useForm` — but without the reactive form object:
const form =useForm("/?create-user", { name: "", email: "" });
247
+
248
+
// Programmatic call + Inertia page update (no form state)
249
+
awaitaction("/?delete-user", { id: 42 });
250
+
```
251
+
252
+
`action()` is useful in event handlers — button clicks, confirmation dialogs, or anywhere you want to call a server action without managing form state.
253
+
158
254
## Validation Errors
159
255
160
256
When an action throws a `ValidationError` (or validation fails via `withValidator`), the errors are automatically available on `form.errors` — no manual error handling needed.
Use `useForm` when you have a form with inputs. Use `action()` for programmatic mutations (delete buttons, toggles). Use `fetch()` when you need the raw response and don't want Inertia page updates.
Layouts nest automatically (e.g. root layout wraps `users/layout` wraps page component) and persist across navigations within their subtree — component state is preserved.
168
+
When a page renders, the layout wraps it — the page component is injected as the layout's children (React/Solid/Svelte) or slot (Vue):
0 commit comments