Guides
Mutation Server Functions
Submit forms and render the current route after a server-side change.
Server Functions let forms and Client Components invoke Effect handlers on the server, where schemas validate their input.
Start with a form that sends a name directly to a Server Function, then add a reply and pending state with useActionState.
Share the application definition
Create src/effront.ts and import this same EFFRONT in the application entry and Server Function modules:
import { Application } from "@effront/core";
export const EFFRONT = Application.effront();For actions that need application services, declare and provide them as in the services guide.
Submit a form directly
Pass the Server Function directly to the form's action.
Its handler returns an Effect<void>.
Create src/record-name.ts with "use server" first:
"use server";
import { Effect, Schema } from "effect";
import { EFFRONT } from "./effront";
export const recordName = EFFRONT.ServerFn.make({
input: Schema.fromFormData(Schema.Struct({ name: Schema.NonEmptyString })),
handler: ({ name }) => Effect.logInfo("Name submitted", { name }),
});Render and submit the form
Update src/entry.effront.tsx to import the shared definition and render the form:
// src/entry.effront.tsx: replace the Application import.
import { EFFRONT } from "./effront";
// Add to the imports.
import { recordName } from "./record-name";
// Remove the local EFFRONT declaration.
// Replace HomePage.
const HomePage = EFFRONT.Page.make({
render: () =>
Effect.succeed(
<form action={recordName}>
<label>
Name
<input name="name" required />
</label>
<button type="submit">Record name</button>
</form>,
),
});Open /, enter Ada, and select Record name.
Submission writes Name submitted to the server log.
It does not persist data or display a completion message.
Add form state with useActionState
To display the server's reply and disable the button while submitting, add a state-returning Server Function and a Client Component.
Create src/greet.ts with "use server" first:
"use server";
import { Effect, Schema } from "effect";
import { EFFRONT } from "./effront";
export const greet = EFFRONT.ServerFn.make({
input: [
Schema.Struct({ message: Schema.String }),
Schema.fromFormData(Schema.Struct({ name: Schema.NonEmptyString })),
],
handler: (_previousState, { name }) => Effect.succeed({ message: `Hello, ${name}.` }),
});useActionState sends the previous state, then FormData.
The input array validates those arguments in the same order before the handler runs.
Create src/greeting-form.tsx:
"use client";
import { useActionState } from "react";
import { greet } from "./greet";
export function GreetingForm() {
const [state, formAction, pending] = useActionState(greet, { message: "" });
return (
<form action={formAction}>
<label>
Name
<input name="name" required />
</label>
<button disabled={pending} type="submit">
Greet
</button>
<p aria-live="polite">{state.message}</p>
</form>
);
}Pass greet directly to useActionState, without an async wrapper, to preserve form submission before JavaScript loads.
required checks the field in the browser.
The Schema still validates it on the server.
In src/entry.effront.tsx, replace the direct form with GreetingForm:
// src/entry.effront.tsx: replace the recordName import.
import { GreetingForm } from "./greeting-form";
// Replace HomePage.
const HomePage = EFFRONT.Page.make({
render: () => Effect.succeed(<GreetingForm />),
});Open /, enter Ada, and select Greet.
The form displays Hello, Ada. after submission.
Accept an object argument
For a Client Component event handler that already has an object, use Schema.Struct directly.
Create src/greet-object.ts:
"use server";
import { Effect, Schema } from "effect";
import { EFFRONT } from "./effront";
export const greetObject = EFFRONT.ServerFn.make({
input: Schema.Struct({ name: Schema.NonEmptyString }),
handler: ({ name }) => Effect.succeed(`Hello, ${name}.`),
});A client caller can import it and call await greetObject({ name: "Ada" }).
Callers pass encoded input and handlers receive decoded values.
Handle updates
After a successful call, Effront renders the current route again. Read persisted values in the Page so that refresh shows the update.
Handle failures
Return expected business failures as state so useActionState can display them.
In src/greet.ts, treat Admin as a reserved name:
// src/greet.ts: replace greet to return an expected failure as form state.
export const greet = EFFRONT.ServerFn.make({
input: [
Schema.Struct({ message: Schema.String }),
Schema.fromFormData(Schema.Struct({ name: Schema.NonEmptyString })),
],
handler: (_previousState, { name }) => {
if (name === "Admin") {
return Effect.succeed({ message: "That name is reserved." });
}
return Effect.succeed({ message: `Hello, ${name}.` });
},
});Enter Admin and select Greet to display That name is reserved..
Enter Ada and submit again to display Hello, Ada..
The reserved-name branch returns state, so it updates state.message instead of failing the action.
Schema decoding failures prevent the handler from running.
For example, the existing Schema.NonEmptyString rejects an empty name before it reaches either branch.
Decoding and handler failures become action failures, not automatic updates to state.message.
If invalid fields need inline feedback, choose a Schema that lets the handler receive and report those values rather than rejecting them first.
For the shared distinction between expected outcomes and operational failures, see Error handling for Server Functions.
See React's useActionState reference for state and error handling.