Forms
StateMesh forms are backed by mesh state. They support async field-level validation, schema adapters, server error mapping, dirty tracking, field arrays, autosave, and multi-step wizards.
Define a Form
mesh.form("profile.form", {
initialValues: {
name: "",
email: "",
links: [] as Array<{ url: string }>
},
fields: {
name(value) {
return value.trim() ? null : "Name is required";
},
async email(value) {
if (!value.includes("@")) return "Valid email is required";
const available = await api.get<{ available: boolean }>("/users/email", {
query: { email: value }
});
return available.available ? null : "Email is already taken";
}
},
validateOnBlur: true,
clearServerErrorOnChange: true,
validate(values) {
return {
...(values.name.length > 80 ? { name: "Name is too long" } : {})
};
},
submit: updateProfileMutation,
mapServerErrors(error) {
const cause = error.cause;
return cause instanceof ApiClientError && cause.status === 422
? { email: "Email is already taken" }
: {};
},
autosave: {
debounce: 800,
validate: true,
when: (form) => form.dirty && !form.submitting
},
steps: [
{ name: "profile", fields: ["name"] },
{ name: "contact", fields: ["email"] }
]
});Use in React
import { useMeshForm } from "statemesh-core";
function ProfileForm() {
const form = useMeshForm<{
name: string;
email: string;
links: Array<{ url: string }>;
}>("profile.form");
return (
<form onSubmit={(e) => void form.submit(e).catch(() => undefined)}>
<input {...form.field("name")} />
{form.touched.name && form.errors.name && <p>{form.errors.name}</p>}
<input {...form.field("email")} />
{form.validatingFields.email && <p>Checking email...</p>}
{form.errors.email && <p>{form.errors.email}</p>}
{form.fieldArray("links").items.map((link, index) => (
<input
key={index}
value={link.url}
onChange={(e) => form.fieldArray("links").update(index, { url: e.target.value })}
/>
))}
{form.autosaving && <p>Saving draft...</p>}
<button disabled={form.submitting}>
{form.submitting ? "Saving..." : "Save"}
</button>
</form>
);
}Field Props
form.field("email") returns input props for React fields:
const props = form.field("email");
// { value, onChange, onBlur, name }Input Helpers
<input type="checkbox" {...form.checkbox("alerts")} />
<input type="radio" {...form.radio("plan", "enterprise")} />
<select {...form.select("country")} />
<input type="file" {...form.file("avatar")} />Schema Adapters
Use Zod, Yup, or Standard Schema for validation:
import { zodSchema, yupSchema, standardSchema } from "statemesh-core";
// Zod
mesh.form("profile.form", {
schema: zodSchema(profileSchema),
// ...
});
// Yup
mesh.form("profile.form", {
schema: yupSchema(profileSchema),
// ...
});
// Standard Schema (Zod 4, Valibot, ArkType, etc.)
mesh.form("profile.form", {
schema: standardSchema(profileSchema),
// ...
});Field Arrays
Manage dynamic lists of fields:
const links = form.fieldArray("links");
links.items; // Array of items
links.append({ url: "" }); // Add to end
links.insert(0, { url: "" }); // Insert at index
links.update(0, { url: "..." }); // Update at index
links.remove(0); // Remove at index
links.move(0, 1); // Move from index 0 to 1
links.replace([{ url: "..." }]); // Replace all itemsAutosave
Configure automatic saving:
{
autosave: {
debounce: 800, // Debounce delay in ms
validate: true, // Validate before saving
when: (form) => form.dirty && !form.submitting // Conditional
}
}// Check autosave status
if (form.autosaving) return <p>Saving draft...</p>;
// Force autosave
form.autosaveNow();Multi-Step Forms
Define steps and navigate between them:
{
steps: [
{ name: "profile", fields: ["name", "bio"] },
{ name: "contact", fields: ["email", "phone"] },
{ name: "preferences", fields: ["theme", "lang"] }
]
}form.currentStep; // "profile"
form.stepFields; // ["name", "bio"]
form.nextStep();
form.previousStep();
form.goToStep("contact");Server Errors
Set server errors separately from client errors:
form.setServerErrors({ email: "Already taken" });Reset to Server Data
Replace values and use the payload as the new dirty baseline:
form.resetToServer(serverProfile);Form State
| Field | Type | Description |
|---|---|---|
values | T | Current form values |
errors | FormErrors<T> | Validation errors |
touched | FormTouched<T> | Which fields have been touched |
dirty | boolean | True when values differ from initial |
dirtyFields | Record<string, boolean> | Per-field dirty state |
submitting | boolean | True during submission |
validating | boolean | True during async validation |
validatingFields | Record<string, boolean> | Per-field validation state |
isValid | boolean | True when no errors and not validating |
autosaving | boolean | True during autosave |
currentStep | string | Current step name |
submit | (event) => Promise<void> | Submit the form |
reset | () => void | Reset to initial values |
Important Notes
TIP
Field-level validators can be async. StateMesh debounces them and tracks per-field validation state.
WARNING
clearServerErrorOnChange: true clears server errors when the user edits the field. This prevents stale error messages from confusing users.
TIP
Forms are registered on the mesh like actions and transactions. Use { replace: true } to override in tests or HMR.
Next Steps
- URL State — State synced to URL
- Error Boundaries — Suspense error handling
- Mutations — Form submission with optimistic updates
