Skip to content

Commit e1453fe

Browse files
yyx990803claude
andcommitted
docs: restructure pages routing docs
- 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>
1 parent ebed48a commit e1453fe

9 files changed

Lines changed: 400 additions & 306 deletions

File tree

‎docs/.vitepress/config.ts‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -179,10 +179,9 @@ export default extendConfig(
179179
collapsed: true,
180180
items: [
181181
{ text: "Overview", link: "/guide/pages-routing/overview" },
182-
{ text: "Loaders & Actions", link: "/guide/pages-routing/loaders-actions" },
183182
{ text: "Layouts & Shared Data", link: "/guide/pages-routing/layouts" },
184-
{ text: "Forms", link: "/guide/pages-routing/forms" },
185-
{ text: "Deferred Props", link: "/guide/pages-routing/deferred-props" },
183+
{ text: "Loaders & Props", link: "/guide/pages-routing/loaders" },
184+
{ text: "Actions & Forms", link: "/guide/pages-routing/actions-and-forms" },
186185
{ text: "View Transitions", link: "/guide/pages-routing/view-transitions" },
187186
{ text: "Head Management", link: "/guide/pages-routing/head" },
188187
{ text: "Islands", link: "/guide/pages-routing/islands" },

docs/guide/pages-routing/forms.md renamed to docs/guide/pages-routing/actions-and-forms.md

Lines changed: 110 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,80 @@
22
outline: deep
33
---
44

5-
# Forms
5+
# Actions & Forms
66

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

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+
export const action = defineHandler.withValidator({
20+
body: insertUserSchema,
21+
})(async (c, { body }) => {
22+
await db.insert(users).values(body);
23+
// No return → re-runs loader, page re-renders with fresh data
24+
});
25+
```
26+
27+
| Action returns | Behavior |
28+
| --------------------- | ----------------------------------------------------- |
29+
| 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+
export const actions = {
43+
update: defineHandler.withValidator({
44+
body: updateUserSchema,
45+
})(async (c, { body }) => {
46+
await db
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 } = await c.req.json<{ id: string }>();
54+
await db.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+
export const 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.
1079

1180
First, define the action in your server handler:
1281

@@ -133,7 +202,7 @@ export default function CreateUser() {
133202

134203
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.
135204

136-
## API Reference
205+
### `useForm` API
137206

138207
`useForm` returns a reactive object with:
139208

@@ -155,6 +224,33 @@ const form = useForm("/users/:id", { name: "" }, { params: { id: "42" } });
155224
form.put(); // submits to /users/42
156225
```
157226

227+
### Named Actions with `useForm`
228+
229+
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:
241+
242+
```ts
243+
import { useForm, action } from "void/react"; // or "void/vue", "void/svelte", "void/solid"
244+
245+
// Form with state + Inertia page update
246+
const form = useForm("/?create-user", { name: "", email: "" });
247+
248+
// Programmatic call + Inertia page update (no form state)
249+
await action("/?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+
158254
## Validation Errors
159255

160256
When an action throws a `ValidationError` (or validation fails via `withValidator`), the errors are automatically available on `form.errors` — no manual error handling needed.
@@ -284,3 +380,13 @@ export const action = defineHandler(async (c) => {
284380
}
285381
});
286382
```
383+
384+
## Choosing a Primitive
385+
386+
| Primitive | Inertia page update | Form state | Framework-specific |
387+
| ---------- | ------------------- | -------------------------- | ------------------ |
388+
| `useForm` | Yes | Yes (errors, dirty, reset) | Yes |
389+
| `action()` | Yes | No | Yes |
390+
| `fetch()` | No (raw response) | No | No |
391+
392+
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.
Lines changed: 34 additions & 0 deletions
Loading

‎docs/guide/pages-routing/layouts.md‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,11 @@ export default function Layout(props: { children: JSX.Element }) {
165165

166166
:::
167167

168-
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):
169+
170+
<img src="./layout-nesting.svg" alt="Layout nesting diagram: pages/layout wraps pages/users/layout wraps pages/users/[id] page component" style="max-width: 520px; width: 100%;" />
171+
172+
Layouts nest automatically and persist across navigations within their subtree — component state is preserved.
169173

170174
## Named Layouts
171175

‎docs/guide/pages-routing/loaders-actions.md‎

Lines changed: 0 additions & 134 deletions
This file was deleted.

0 commit comments

Comments
 (0)