dev3 ·
I have a contact form in the App Router and I would like to avoid writing an API route. How do I write a Server Action, show validation errors and a pending state?
0
DEV3 Developer Forum
Every post is reviewed first. Usually reviewed within 24 hours.
I have a contact form in the App Router and I would like to avoid writing an API route. How do I write a Server Action, show validation errors and a pending state?
A Server Action is an async function marked "use server" that runs on the server and can be passed to a form's action.
// app/contact/actions.ts
"use server";
import { z } from "zod";
const schema = z.object({
email: z.string().email(),
message: z.string().min(10),
});
export type State = { ok?: boolean; errors?: Record<string, string[]> };
export async function sendMessage(_prev: State, formData: FormData): Promise<State> {
const parsed = schema.safeParse(Object.fromEntries(formData));
if (!parsed.success) {
return { errors: parsed.error.flatten().fieldErrors };
}
// save to DB or send mail here
return { ok: true };
}
// app/contact/form.tsx
"use client";
import { useActionState } from "react";
import { useFormStatus } from "react-dom";
import { sendMessage } from "./actions";
function Submit() {
const { pending } = useFormStatus();
return <button disabled={pending}>{pending ? "Sending..." : "Send"}</button>;
}
export default function Form() {
const [state, action] = useActionState(sendMessage, {});
return (
<form action={action}>
<input name="email" />
{state.errors?.email && <p>{state.errors.email[0]}</p>}
<textarea name="message" />
<Submit />
{state.ok && <p>Sent.</p>}
</form>
);
}
useActionState is the React 19 name (it was useFormState in React 18 canaries).revalidatePath or revalidateTag after mutating data so cached pages update.Pitfall: defining zod schemas with inline .refine inside a "use server" file can break compilation in some setups. Keep schemas in a separate module and import them.