A more seamless way to build React forms from Zod schemas
- 100% typesafe - Fully typechecked paths, input and output types for deeply nested fields
- Supports
z.string().optional()
,z.string().nullable()
,z.number()
etc in inputs out of the box - Interprets blank inputs as
undefined
ornull
by default, depending on what the field schema accepts - Normalizes inputs on blur by default (e.g. with
z.string().blur()
you'll see the trim on blur) - Supports Zod schemas with different input and output types (as long as you use
zod-invertible
to specify how to format from output back to input) - Allows you to programmatically set either input or output values
- Each step of a wizard form can declare its own submit handler independent from the enclosing
<form>
element. This enables animated transitions between steps without a separate<form>
s or submit button for each step.
- Designed specifically for Zod and React only
- Not currently focused on high performance/large form state like
final-form
orreact-hooks-form
- No async validate outside of submit right now
pnpm i @jcoreio/zod-forms
or if you're using npm
:
npm i --save @jcoreio/zod-forms
In this example, we'll have a url
field that must be a valid URL.
Using .trim()
ensures that the submitted value will be trimmed.
The displayed value will also be trimmed whenever the field is blurred.
import z from 'zod'
const schema = z.object({
url: z.string().trim().url(),
})
import { createZodForm } from '@jcoreio/zod-form'
const {
FormProvider,
// all of the following hooks can also be imported from '@jcoreio/zod-form',
// but the ones returned from `createZodForm` are already bound to the schema type
useInitialize,
useSubmit,
useFormStatus,
useHtmlField,
} = createZodForm({ schema })
import { FieldPathForRawValue } from '@jcoreio/zod-form'
function FormInput({
field,
type,
...props
}: Omit<React.InputHTMLAttributes<HTMLInputElement>, 'type'> & {
type: HTMLInputTypeAttribute
// This ensures that only fields that accept string, null or undefined
// as input can be passed to <FormInput>
field: FieldPathForRawValue<string | null | undefined>
}) {
// This hook is designed to provide the smoothest integration with simple <input>s.
const { input, meta } = useHtmlField({ field, type })
const inputRef = React.createRef<HTMLInputElement>()
const error = meta.touched || meta.submitFailed ? meta.error : undefined
React.useEffect(() => {
inputRef.current?.setCustomValidity(error || '')
}, [error])
return (
<input
{...props}
// the `input` props from `useHtmlField` are designed to be spread here
{...input}
ref={inputRef}
/>
)
}
function MyForm() {
return (
// <FormProvider> wraps <MyFormContent> in a React Context through which the
// hooks and fields access form state
<FormProvider>
<MyFormContent />
</FormProvider>
)
}
function MyFormContent() {
// This hook initializes the form with the given values.
// The second argument is a dependency array -- the form will be reinitialized
// if any of the dependencies change, similar to React.useEffect.
useInitialize({ values: { url: 'https://localhost' } }, [])
// This hook sets your submit handler code, and returns an onSubmit handler to
// pass to a <form>
const onSubmit = useSubmit({
onSubmit: async ({ url }) => {
alert(`Submitted! url value: ${url}`)
},
})
const { submitting, pristine } = useFormStatus()
return (
<form onSubmit={onSubmit}>
<FormInput
// this is how we bind <FormInput> to the `url` field
field={myForm.get('url')}
type="text"
placeholder="URL"
/>
<button disabled={pristine || submitting} type="submit">
submit
</button>
</form>
)
}