Skip to content

Commit 48c8de3

Browse files
JoviDeCroockclaude
andcommitted
Remove action() export convention from route modules
Forms now POST to API routes directly instead of page route actions. Simplify <Form> component to a thin fetch+redirect wrapper without automatic revalidation. Add useRevalidate() hook (useRevalidateRoute kept as deprecated alias). Remove ActionArgs, ActionEnvelope, ActionFn, ActionResult types, useSubmitAction hook, CSRF validation on page routes, action dispatch in handleViactRequest, and revalidation hints. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent edeafc9 commit 48c8de3

22 files changed

Lines changed: 249 additions & 565 deletions

docs/ARCHITECTURE.md

Lines changed: 3 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,12 @@ export const app = defineApp({
6262
}),
6363
]),
6464
group({ shell: "app", middleware: ["auth"] }, [
65-
// Inline style: loader/action exported from the route file
65+
// Inline style: loader exported from the route file
6666
route("/settings", "./routes/settings.tsx", { render: "spa" }),
6767
// Separate files style: server code in dedicated files
6868
route("/dashboard", {
6969
component: "./routes/dashboard.tsx",
7070
loader: "./server/dashboard-loader.ts",
71-
action: "./server/dashboard-action.ts",
7271
render: "ssr",
7372
}),
7473
]),
@@ -92,7 +91,7 @@ assignment implicit via `_middleware.ts` files. Viact's hybrid approach:
9291
Viact supports two styles for wiring data loading to routes. Both can coexist
9392
in the same app.
9493

95-
#### Style A: Inline (loader/action in the route file)
94+
#### Style A: Inline (loader in the route file)
9695

9796
A route module exports some combination of:
9897

@@ -104,13 +103,6 @@ export async function loader({ request, params, context, signal }: LoaderArgs) {
104103
return { user: await getUser(request) };
105104
}
106105

107-
// Server: handles POST/PUT/PATCH/DELETE
108-
export async function action({ request, params, context }: ActionArgs) {
109-
const form = await request.formData();
110-
await createProject(form.get("name"));
111-
return { ok: true, revalidate: ["route:self"] };
112-
}
113-
114106
// Shared: <head> metadata
115107
export function head({ data }: HeadArgs<typeof loader>) {
116108
return { title: `Dashboard — ${data.user.name}` };
@@ -145,15 +137,6 @@ export async function loader({ request }: LoaderArgs) {
145137
}
146138
```
147139

148-
```typescript
149-
// src/server/dashboard-action.ts
150-
export async function action({ request }: ActionArgs) {
151-
const form = await request.formData();
152-
await createProject(form.get("name"));
153-
return { ok: true, revalidate: ["route:self"] };
154-
}
155-
```
156-
157140
```typescript
158141
// src/routes/dashboard.tsx — pure component, no server code
159142
export function Component({ data }: RouteComponentProps) {
@@ -167,7 +150,6 @@ Wired in the manifest via the `RouteConfig` object form:
167150
route("/dashboard", {
168151
component: "./routes/dashboard.tsx",
169152
loader: "./server/dashboard-loader.ts",
170-
action: "./server/dashboard-action.ts",
171153
render: "ssr",
172154
})
173155
```
@@ -295,19 +277,6 @@ User clicks <a> or calls navigate()
295277
This "server-owned navigation" pattern means loaders never run in the browser.
296278
Secrets in loader code stay server-side. The client only receives serialized data.
297279

298-
### Action Submission
299-
300-
```
301-
User submits <Form> or calls submitAction()
302-
→ POST to current route URL
303-
→ Server validates same-origin (CSRF)
304-
→ Run middleware
305-
→ Execute action function
306-
→ If revalidate hints: re-run affected loaders
307-
→ Return JSON response
308-
→ Client updates data and re-renders
309-
```
310-
311280
---
312281

313282
## Build Pipeline
@@ -325,7 +294,7 @@ Viact uses Vite's multi-environment build:
325294
- Entry: `virtual:viact/server`
326295
- Output: `dist/ssr/` or `dist/server/`
327296
- Produces: route manifest JSON, ISG manifest JSON
328-
- Contains: loader/action/shell/middleware code
297+
- Contains: loader/shell/middleware code
329298

330299
3. **platform** (adapter-specific) — entry module
331300
- Entry: `virtual:viact/node-server` or `virtual:viact/cloudflare-worker`

docs/DATA_LOADING.md

Lines changed: 7 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Data Loading
22

33
Viact provides a unified data loading model that works across all rendering modes.
4-
Loaders fetch data, actions handle mutations, and client hooks provide reactive access.
4+
Loaders fetch data and client hooks provide reactive access.
55

66
---
77

@@ -67,64 +67,6 @@ the fallback UI. Otherwise, the error bubbles to the shell or global handler.
6767

6868
---
6969

70-
## Actions
71-
72-
Actions handle form submissions and mutations. They receive POST, PUT, PATCH,
73-
or DELETE requests.
74-
75-
```typescript
76-
export async function action({ request, params, context }: ActionArgs) {
77-
const form = await request.formData();
78-
const name = String(form.get("name") || "").trim();
79-
80-
if (!name) {
81-
return { ok: false, data: { error: "Name is required" } };
82-
}
83-
84-
await db.projects.create({ name });
85-
return { ok: true, revalidate: ["route:self"] };
86-
}
87-
```
88-
89-
### ActionArgs
90-
91-
Same as `LoaderArgs``request`, `params`, `context`, `signal`, `url`, `route`.
92-
93-
### Return values
94-
95-
Actions can return:
96-
97-
| Return | Effect |
98-
| -------------------------- | ----------------------------------------- |
99-
| Plain data | Serialized to client as JSON |
100-
| `{ ok, data, revalidate }` | Structured result with revalidation hints |
101-
| `{ redirect: "/path" }` | Server-side redirect |
102-
| `{ data, headers }` | Custom response headers (cookies, cache) |
103-
104-
### Revalidation hints
105-
106-
After a mutation, tell viact which routes need fresh data:
107-
108-
```typescript
109-
return {
110-
ok: true,
111-
revalidate: ["route:self"], // Re-run this route's loader
112-
// revalidate: ["route:dashboard"], // Re-run a specific route by ID
113-
};
114-
```
115-
116-
### CSRF protection
117-
118-
Page actions validate same-origin requests automatically. Unsafe requests must
119-
send an `Origin` or `Referer` header that matches the current route origin or
120-
they are rejected with `403`.
121-
122-
API routes are separate endpoints. They do not inherit page-route middleware or
123-
page-action CSRF behavior automatically. If you want shared API policy, declare
124-
it explicitly with `defineApp({ api: { middleware: ["auth"] } })`.
125-
126-
---
127-
12870
## Head Metadata
12971

13072
The `head` export controls `<head>` content per route:
@@ -161,36 +103,27 @@ export function Component() {
161103
}
162104
```
163105

164-
### `useRevalidateRoute()`
106+
### `useRevalidate()`
165107

166108
Imperatively re-run the current route's loader:
167109

168110
```typescript
169111
export function Component() {
170-
const revalidate = useRevalidateRoute();
112+
const revalidate = useRevalidate();
171113
return <button onClick={() => revalidate()}>Refresh</button>;
172114
}
173115
```
174116

175-
### `useSubmitAction()`
176-
177-
Submit an action programmatically:
178-
179-
```typescript
180-
const submit = useSubmitAction();
181-
await submit({ method: "POST", body: formData });
182-
```
183-
184117
### `<Form>` Component
185118

186-
Declarative form submission that calls the route's action:
119+
Declarative form submission:
187120

188121
```typescript
189122
import { Form } from "viact";
190123

191124
export function Component() {
192125
return (
193-
<Form method="post">
126+
<Form method="post" action="/api/projects">
194127
<input name="title" />
195128
<button type="submit">Create</button>
196129
</Form>
@@ -200,8 +133,8 @@ export function Component() {
200133

201134
The `<Form>` component:
202135

203-
- Intercepts submit and sends via fetch (no full page reload)
204-
- Automatically revalidates based on action response hints
136+
- Intercepts submit and sends via fetch to the specified action URL (no full page reload)
137+
- Handles redirects automatically
205138
- Falls back to native form submission if JavaScript fails
206139

207140
---

e2e/basic.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ test("dashboard renders with session cookie", async ({ page, context }) => {
7777
await expect(page.locator("p")).toContainText("Projects: 3");
7878
});
7979

80-
test("dashboard form submits in-app and keeps the current route hydrated", async ({
80+
test("dashboard form posts to API route and keeps the current route hydrated", async ({
8181
page,
8282
context,
8383
}) => {
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export async function POST() {
2+
return Response.json({ saved: true });
3+
}

examples/basic/src/routes/dashboard.tsx

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Form, type LoaderArgs, type RouteComponentProps } from "viact";
1+
import { Form, useRevalidate, type LoaderArgs, type RouteComponentProps } from "viact";
22

33
export async function loader({ request }: LoaderArgs) {
44
const hasSession = request.headers.get("cookie")?.includes("session=") ?? false;
@@ -9,20 +9,20 @@ export async function loader({ request }: LoaderArgs) {
99
};
1010
}
1111

12-
export async function action() {
13-
return {
14-
data: { saved: true },
15-
ok: true,
16-
revalidate: ["route:self"],
17-
};
18-
}
19-
2012
export function Component({ data }: RouteComponentProps<typeof loader>) {
13+
const revalidate = useRevalidate();
14+
2115
return (
2216
<section>
2317
<h1>{data.user}</h1>
2418
<p>Projects: {data.projectCount}</p>
25-
<Form method="post">
19+
<Form
20+
method="post"
21+
action="/api/dashboard"
22+
onSubmit={async () => {
23+
await revalidate();
24+
}}
25+
>
2626
<button type="submit">Revalidate dashboard</button>
2727
</Form>
2828
</section>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export async function POST() {
2+
return Response.json({ saved: true });
3+
}

examples/cloudflare/src/routes/dashboard.tsx

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Form, type LoaderArgs, type RouteComponentProps } from "viact";
1+
import { Form, useRevalidate, type LoaderArgs, type RouteComponentProps } from "viact";
22

33
export async function loader({ request }: LoaderArgs) {
44
const hasSession = request.headers.get("cookie")?.includes("session=") ?? false;
@@ -9,20 +9,20 @@ export async function loader({ request }: LoaderArgs) {
99
};
1010
}
1111

12-
export async function action() {
13-
return {
14-
data: { saved: true },
15-
ok: true,
16-
revalidate: ["route:self"],
17-
};
18-
}
19-
2012
export function Component({ data }: RouteComponentProps<typeof loader>) {
13+
const revalidate = useRevalidate();
14+
2115
return (
2216
<section>
2317
<h1>{data.user}</h1>
2418
<p>Projects: {data.projectCount}</p>
25-
<Form method="post">
19+
<Form
20+
method="post"
21+
action="/api/dashboard"
22+
onSubmit={async () => {
23+
await revalidate();
24+
}}
25+
>
2626
<button type="submit">Revalidate dashboard</button>
2727
</Form>
2828
</section>

examples/docs/src/routes/docs/adapters.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ Keep your `wrangler.jsonc` in the project root so you can add bindings without t
6767

6868
### Accessing Cloudflare bindings
6969

70-
The `env` object is passed through to your loaders and actions via the context:
70+
The `env` object is passed through to your loaders and API routes via the context:
7171

7272
```ts
7373
// src/routes/dashboard.tsx
@@ -151,7 +151,7 @@ node dist/server/server.js
151151

152152
## Context Factory
153153

154-
Adapters inject platform-specific values into loaders and actions via a context factory. This is where you connect database clients, environment bindings, and other platform resources:
154+
Adapters inject platform-specific values into loaders and API routes via a context factory. This is where you connect database clients, environment bindings, and other platform resources:
155155

156156
```ts
157157
// Node: inject a database pool
@@ -169,7 +169,7 @@ createContext: ({ request, env, executionContext }) => ({
169169
})
170170
```
171171

172-
The context object is available as `args.context` in every loader, action, middleware, and API route handler.
172+
The context object is available as `args.context` in every loader, middleware, and API route handler.
173173

174174
---
175175

0 commit comments

Comments
 (0)