How to create composable forms using React Hook Form, Compound Components and Zod
Tags: react, hook, typescript, zod
Published: 2022-08-03 03:01:28

---
title: How to create composable forms using React Hook Form, Compound Components and Zod
published: true
description: Explains how to create reuseable forms with React Hook Form, Zod and advanced React patterns.
tags: react, hook, typescript, zod
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x7htynw79cozmrxp6nvr.png
# Use a ratio of 100:42 for best results.
---

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](https://www.smashingmagazine.com/2021/08/compound-components-react/). 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.
```Typescript
<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](https://react-hook-form.com/advanced-usage#SmartFormComponent). Here's a screenshot of it.
![Screenshot of smart form component on React Hook Form website](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/a26mlt4j7evon9z1anx7.png)

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. 
![Code screenshot of how limited is the Form from their webiste](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/knmi0ftuuvpzv5rru8bl.png)
![Animation of trying to set a div in the form and it gives an error](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5il1t3bdsdpd07dcsvmd.gif)

This can be circumvented by using another API from their documentation.

## Use form context
Using [form context](https://react-hook-form.com/api/useformcontext) allows to create child components no matter how deep they are. You wrap your form with a `<FormProvider>` passing all the methods
```Typescript
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.
```Typescript
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](https://zod.dev/)  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.

```Typescript
+ import { zodResolver } from "@hookform/resolvers/zod"
...
function Form({
+ schema,
...
}: {
+ schema: any
...
}
  const methods = useForm({
    defaultValues,
+    resolver: zodResolver(schema)
  })
```

```Typescript
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.
![Validation highlight of passing invalid name for input](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l4myhj7hgbgz1bfvopdg.png)
 
[You can find the live example here](https://codesandbox.io/s/react-form-hook-zod-ngylel?file=/src/components/ReactForm.tsx)