Skip to content

Instantly share code, notes, and snippets.

@dferber90
Created January 27, 2021 16:00
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dferber90/3001450d24f4f4ec179d2bfe92c6dc1f to your computer and use it in GitHub Desktop.
Save dferber90/3001450d24f4f4ec179d2bfe92c6dc1f to your computer and use it in GitHub Desktop.
useZodForm

Motivation

To refresh your memory, Formik uses an error shape like so

{ name: "No name provided", "age": "You must be at least 18 years old" }

I love formik, but I think it is a design flaw that its default errors shape only allows for one error per field. You might have multiple errors at one field.

We can solve that by using zod's error approach of using an array with paths. The same errors could look like this:

[
  { path: ["name"], code: "too_small", message: "Should be at least 1 characters" },
  { path: ["age"], code: "too_small", message: "Value should be greater than or equal to 18" },
]

Something that's hard in Formik is if your error can appear at a nested field or above it, as the default errors shape can't handle that.

Imagine a list of guests where each guest must have name, and where you must invite at least three guests.

{ guests: [{ name: "jon" }, { name: "" }] }

I couldn't find a way/it seems very hard/not possible to handle this case with Formik's own shape, although I found that this comes up quite a bit.

With zod's errors shape this is trivial:

[
 { path: ["guests"], code: "custom_error", message: "Must invite at least three guests" },
 { path: ["guests", 1, "name], code: "too_small", message: "Should be at least 1 characters" },
]

By changing the shape from Formik's default to an array of paths (an array of z.Suberror to be exact), we can deal with errors at all levels easily.

Additional cool stuff

We can add helper functions to our form library to deal with these arrays of errors.

zodForm.errors is an array of z.Suberror, so it looks like this

[{ path: ["name"], code: "custom_error", message: "some custom message" }]

To check if a field has an error, you can do

zodForm.hasErrors({ path: ["name"] })

This will return all errors at that path or its subpaths but you can also restricit it to errors of that exact path only like so:

zodForm.hasErrors({ path: ["name"] }, { exact: true })

What's super useful is that you can query your errors:

zodForm.hasErrors({ path: ["name"], code: "custom_error", message: "some custom message" })
// returns true if there is at least one error at the path (or its subpath) with the provided values for code and message.

The same API works for receiving those errors

zodForm.getErrors({ path: ["name"], code: "custom_error", message: "some custom message" })
// returns all errors whose path starts with "name" and who'se code and message match the provided values.
import * as z from "zod"
const yourFormValuesShape = z.object({
name: z.string().min(1),
age: z.union([z.number().min(18), z.literal("")])
})
type YourFormValuesShape = z.infer<typeof yourFormValuesShape>
function UserForm () {
const zodForm = useZodForm<YourFormValuesShape>({
initialValues: { name: "", age: "" },
validationSchema: yourFormValuesSchema,
onSubmit: (values, bag) => {
console.log(values)
bag.setSubmitting(false);
bag.resetForm();
},
});
return (
<form onSubmit={zodForm.handleSubmit}>
<input name="name" type="text" value={formik.values.name} onChange={zodForm.handleChange} />
{zodForm.hasError({ path: ["name"] }) && <p>Something is wrong with the name</p>}
<input name="age" type="number" value={formik.values.age} onChange={zodForm.handleChange} />
{zodForm.hasError({ path: ["age"], code: "missing" }) && <p>You must specify an age</p>}
{zodForm.hasError({ path: ["age"], code: "custom_error", message: "too young" }) && <p>You must be at least 18</p>}
</form>
)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment