In this article I will show you how to use advanced React concepts to have a form with reusable components, validation and have it share data between components. This will avoid abuse of prop drilling and of context while allowing to compose our form. It will be as much as it can be valid with Typescript.
Compound Components
First Compound Components is a way to explain to the reader there's a parent-child relationship between component. It makes it so that you have to define the parent before you define the child. There's a whole article about that on Smashing Magasine. Basically it allows us to have components like below where you know you have to create a Form component before using the inputs. The reader can also deduce those components are reusable.
<Form onSubmit={onSubmit}>
<Form.Input name="firstName" label="First name" />
<Form.Input name="lastName" label="Last name" />
<Form.Submit type="button"/>
</Form>
Composing your form with reusable components
To make your form reusable, you have to create components that are reusable and also you should be able to compose your form as you need. For this React Hook Form provides a small example. Here's a screenshot of it.
There's a problem with this solution though. It creates every child component passing it the register
function and so it requires that every child component is an HTML input or select.
This can be circumvented by using another API from their documentation.
Use form context
Using form context allows to create child components no matter how deep they are. You wrap your form with a <FormProvider>
passing all the methods
export function Form({
schema,
onSubmit,
children,
defaultValues
}: {
schema: any
onSubmit: (data: Record<string, any>, event?: React.BaseSyntheticEvent) => void
children: any
defaultValues?: Record<string, any>
}) {
const methods = useForm({
defaultValues,
resolver: zodResolver(schema)
})
const handleSubmit = methods.handleSubmit
return (
<FormProvider {...methods}>
<form onSubmit={handleSubmit(onSubmit)}>
{children}
</form>
</FormProvider>
)
}
Now we can have an <Input>
defined as below where we get the register
function that is needed to link the input to the React Hook Form and some other state like errors
and isSubmitting
. With this error handling is within the component and the input gets locked when we submit.
Form.Input = function Input({
name,
displayName,
type
}: {
name: string
displayName: string
type: string
}) {
const {
register,
formState: { isSubmitting, errors }
} = useFormContext()
return (
<div>
<label className="block">
<span className="block">{displayName}</span>
<input
type={type}
{...register(name)}
disabled={isSubmitting}
/>
</label>
{errors[name as string] && (
<p className="error">{errors[name as string]?.message}</p>
)}
</div>
)
}
Assign a schema for validation
For this form to be reusable and valid we want to do validation on the inputs. React Hook Form provide their own simple validation but here we will use zod as schema validation. This makes the form ready to handle more complex validation.
Adding validation can be done by passing the validation schema to the Form
component.
+ import { zodResolver } from "@hookform/resolvers/zod"
...
function Form({
+ schema,
...
}: {
+ schema: any
...
}
const methods = useForm({
defaultValues,
+ resolver: zodResolver(schema)
})
export const FormSchema = z.object({
email: z.string().email(),
username: z.string().min(3, { message: "Must be more than 3 characters" }),
pizzaChoice: z.string(),
accept: z.literal(true, {
errorMap: () => ({
message: "You must accept Terms and Conditions."
})
}),
tier: z
.string({ invalid_type_error: "Please select a payment tier." })
.refine((val) => Tiers.map((tier) => tier.id).includes(val))
})
<Form schema={FormSchema} onSubmit={onSubmit} defaultValues={someInitialValues}>
...
</Form>
Live example with Typescript validation
I have a live example with valid Typescript and the name attributes needs to be one of the keys of the schema.
Top comments (2)
Thankyou great work
In the example you are using controlled inputs, how would it be done with inputs controlled by react-hook-form?