Form validation with Conform, Zod, and Remix
This article was written for Remix 2, which has been merged into React Router 7. Some changes may be necessary.
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")
}Next post
Add Prisma to a Remix app