.cursor/skills/form-submissions/SKILL.md
Form submission patterns using useDynamicSubmitter and formAction. Use when implementing forms, handling form submissions, or processing form data in React Router routes.
npx skillsauth add firtoz/cf-multiworker-boilerplate form-submissionsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Always use submitter.submitJson() instead of React Router's <Form> component.
MaybeError tooMatch actions: loaders should return Promise<MaybeError<LoaderData>> with success / fail, not a bare object. That keeps loaderData typed the same way as submitter/fetcher results and avoids ambiguous error shapes. See .cursor/skills/routing/SKILL.md.
import { formAction, type RoutePath, useDynamicSubmitter } from "@firtoz/router-toolkit";
import { success } from "@firtoz/maybe-error";
import { z } from "zod";
export const route: RoutePath<"/admin/settings"> = "/admin/settings";
export const formSchema = z.object({
siteName: z.string().optional(),
siteUrl: z.string().optional(),
// ... other fields
});
export const action = formAction({
schema: formSchema,
handler: async ({ request }, formData) => {
// formData is typed and validated by Zod
await updateSettings(formData);
// Return success() for successful operations
return success();
// Throw for redirects
// throw redirect("/somewhere");
},
});
Use controlled inputs with React state.
export default function MyForm() {
const submitter = useDynamicSubmitter<typeof import("./my-route")>("/my-route");
const [field1, setField1] = useState("");
const [field2, setField2] = useState("");
const handleSubmit = () => {
submitter.submitJson({
field1,
field2,
});
};
return (
<div>
<Input value={field1} onChange={(e) => setField1(e.target.value)} />
<Input value={field2} onChange={(e) => setField2(e.target.value)} />
<Button onClick={handleSubmit} disabled={submitter.state === "submitting"}>
{submitter.state === "submitting" ? "Saving..." : "Save"}
</Button>
</div>
);
}
<Form> from react-routervalue + onChange)useDynamicSubmitter for form submissionsubmitter.submitJson() with data objectsubmitter.state for loading statessubmitter.dataroute, formSchema, and wrap action with formAction()success() from action handlers for successful operationsthrow (don't return) redirects and responsesThis approach provides:
development
Repo-root commands, typegen and typecheck cadence, lint, deploy, adding packages with bun, and Alchemy app layout. Use at the start of a task, before PR, or when choosing turbo/typegen commands.
development
Fork and template gotchas (env import, routes, typegen, forms, D1, Turbo, HMR, new DO packages). Use when working on apps/web or durable-objects, or when behavior diverges from this stack’s conventions.
testing
Turborepo task configuration patterns for monorepo management. Use when configuring turbo.json tasks, setting up task dependencies, managing cache inputs/outputs, or working with cross-package dependencies in the monorepo.
development
React Router v7 routing patterns and environment variable configuration. Use whenever you touch React Router–related code (routes, links, params, loaders, actions, route config, or env in route context).