Load data and handle forms with Void
Write Void loaders, intent actions, forms, links, and routes.
Put data loading in pages/*.server.ts and rendering in pages/*.tsx. Copy the starter shape below.
// pages/index.server.ts
import { defineHandler } from "void";
import type { InferProps } from "void";
export type Props = InferProps<typeof loader>;
export const loader = defineHandler(async () => ({ items: [] }));
// One action per page; an `intent` field tells the forms apart.
export const action = defineHandler(async (c) => {
const { intent, text } = await c.req.json<{ intent?: string; text?: string }>();
if (intent === "add" && !text?.trim()) {
return c.json({ errors: { text: "Write something first." } }, 422);
}
// Mutate here, then return nothing: Void re-runs the loader in place.
});
import { Form, useForm } from "@ilha/void";
const add = useForm("/" as string, { intent: "add", text: "" });
// add.data.text, add.setData("text", v), add.errors.text, add.pending
Handle each form with its own intent
- Declare one
actionper page and branch on theintentfield. - Return
422with{ errors: { field: "message" } }for validation failures. - Create one
useFormper form with its ownintentvalue. - Render each form with
Formfrom@ilha/void. - Return nothing on success so Void re-runs the loader without a reload.
Route with files and link without reloads
- Map file paths to routes:
pages/index.tsxrenders/. - Prefix shared components with
_; they never become routes. - Wrap pages in
layoutfiles placed beside the routes. - Navigate with
Linkfrom@ilha/void:<Link href="/about" prefetch>About</Link>. - Style with daisyUI classes (
card,btn,input,join,list,badge,alert); reserve Tailwind for layout. - Build island pages as
*.island.tsxwithwith { island: "visible" }imports anduseIslandForm().
For the person-side walkthrough, see Add interactive pages.