Form validation with Conform, Zod, and Remix
Conform is a form validation library that helps you build forms with serverside validation and client-side error handling. It works really well with Remix and Zod.
With Conform, you can represent your form schema with Zod.
import { z } from "zod"const schema = z.object({ title: z.string(), description: z.string().optional(), status: z.enum(["todo", "doing", "done"]),})In your form component, use Conform's useForm hook to get the props you need to make your form work.
- The
onValidatemethod is where we'll make it use the zod schema. - The
lastSubmissionprop takes the response from the action so it can handle errors for us.
import { conform, useForm } from "@conform-to/react"import { getFieldsetConstraint, parse,} from "@conform-to/zod"export default function Example() { const actionData = useActionData<typeof action>() const [form, fields] = useForm({ id: "example", onValidate({ formData }) { return parse(formData, { schema }) }, lastSubmission: actionData?.submission, shouldRevalidate: "onBlur", }) return ( … )}Next, we'll use the fields object to get the props we need for each field in our form.
- Pass
form.propsto the<Form>component. - Each input gets an HTML id generated automatically. Pass
fields.title.idto thehtmlForprop on the<label>to attach it. - Pass
conform.input(fields.title)to the<input>
Each input gets its own list of errors in fields.title.errors.
export default function Example() { const [form, fields] = useForm({ … }) return ( <Form method="POST" {...form.props}> <div> <label htmlFor={fields.title.id}> Title </label> <input {...conform.input(fields.title)} /> </div> <div> <label htmlFor={fields.description.id}> Description </label> <input {...conform.input(fields.description)} /> {fields.description.errors ? ( <div role="alert"> {fields.description.errors[0]} </div> ) : null} </div> <div> <label htmlFor={fields.status.id}> Status </label> <select {...conform.select(fields.status)}> <option value="todo">Todo</option> <option value="doing">Doing</option> <option value="done">Done</option> </select> </div> <button type="submit"> Submit </button> </Form> )}The last step is to create an action that will handle the form submission.
- Use the parse method from
@conform-to/zodwith your schema - A failed submission will have an empty
valueproperty, so you can use that to handle errors. Return the submission object in the response so the form can display the errors. - If the submission is valid, you can use the
valueproperty to get the data you need to enter into your database.
import { parse } from "@conform-to/zod"export async function action({ request }: ActionFunctionArgs) { const formData = await request.formData() const submission = await parse(formData, { schema }) if (!submission.value) { return json( { status: "error", submission }, { status: 400 }, ) } const { title, description, status } = submission.value await db.todos.create({ title, description, status, }) return redirect("/todos")}