# react-hook-form + Zod : Minimal input components and strong typing I tried to find a good pattern to make efficients forms with fields control, strongly typed and minial TSX. Actually, we have many big input components with many props and callbacks. Some use react-hook-form, some not. We have to navigate into multiple places to modify each input and give to them specific logics. The goal of this reflexion is to harmonize and facilitate our forms.* *Instead of using specific logics on each input component, why no just validate fields through the form type ?* *react-hook-form allows to do that using`resolver` in association with a typing library (Zod in our case). Thanks to that, we centralise the form validation in a Zod type. We use `zodResolver` from `@hookform/resolvers/zod` ___ *I hope it's approximately clear and it's a good usage of this pattern, it's a ok solution for our needs, maybe we could have another approach for our forms. But anyway, a refacto is needed, or, at least, a documented standard This document is certainly incomplete* ___ ## Create and use forms ### useForm ```` const form = useForm<FormType>({ mode: "all", resolver: zodResolver(ZodFormType), }); ```` zodResolver is used to plug the ZodFormType validation ### Accessing to the form everywhere with useFormContext Instead of passing `form` as a prop to the bottom #### Wrap the input components into a FormProvider ``` <FormProvider {...form}> ...inputs, form usage </FormProvider> ``` #### Retreive the form in any inputs using useFormContext ``` const collectionForm = useFormContext<FormType>(); ``` ## Correctly type Zod object ### Create Zod form type ``` export const ZodFormtype = z.object({ ...shape }) ``` ### Declare fields #### Optional fields Use `optional()` #### Mandatory fields Use `.min(1)` #### Prevent blank value in fields Use `trim()` #### Refined validation Use `refine()`. It allows to use more versatile validation than just Typescript representation (Using expression) #### Optional field with no error if no value In the case we want to validate the input value and show an error only if there is a value : - `property1: z.string().trim().refine(!value || value => EAIL_REGEXP.test(value)).optional()` #### Custom error messages In most of Zod methods, you can provide a second arguent as error message Examples : - `property2: z.string().trim().min(1, "This field is required")` - `property3: z.string().trim().min(1, "This field is required").refine(value => URL_REGEXP.test(value), "Only URLs are allowed")` ## Minimal usage of TextInput (string value) It's recommended to make a component with these properies : - Generic type - name - form - placeholder - valueModifier - props for style, ... Example : ``` <TextInput placeholder={placeHolder} onChangeText={(text) => valueModifier ? field.onChange(valueModifier(text)) : field.onChange(text)} value={field.value || ""} /> ``` ### useController ``` const { fieldState, field } = useController<FormType>({ name, control: form.control, }); ``` #### name It must match the field name from the FormType declaration #### control Used to plug the form's values to the field #### value Set the TextInput as controlled component. We set the `value` prop to pass a default value `""`. Prevents this error : `A component is changing a controlled input to be uncontrolled` #### valueModifier Used to force the front value. Example : Force the value to be uppercase. We can use `toUppercase()` in the ZodFormType, but it will not modify the front value in the form. So, we can use a `valueModifier`. ### Handle array of fields with useFieldArray If you have an array of objects in FormType like : ``` property4: { label: string description: string }[] ``` You want to display X inputs that match property4 shape. You will use useFieldArray like this : ``` const { fields, update, append, remove } = useFieldArray({ control: form.control, name: "property4", }); ``` And map each items to display one input per item : ``` {fields.map((item, index) => ...TextInput usage with FormType and name (path) as explained above )} ``` #### paths This is the `name`, but for each item. Paths are accessible using the item's index. Typed `Path<FormType>` Example, `name` will be `formProperty4.${itemIndex}.fieldProperty1` #### fields All the property4 values #### update Used to change the property4 value (At itemIndex) #### append Used to add a property4 item #### remove Used to remove a property4 item (From itemIndex) ## Usage of another inputs with <Controller/> You can plug a TextInput to the form using `useController`. But you could want to plug another component as input to the form. Example : A DateTimeInput. To do that, you will wrap the component into `Controller`, and pass the form control, the FormType and the field name to `Controller` Example : ``` <Controller<FormType> control={form.control} name="property5" render={({ field: { onChange } }) => <DateTimeInput onSelectDateTime={dateTime => onChange(dateTime)} /> )} /> ``` ### onChange Used to set the field's value ### value The current field's value ## Display field errors Simply add this near each input component to show the field's errors (Default errors or custom errors specified in ZodFormType) : ``` <ErrorText> {form.getFieldState("property6").error?.message} </ErrorText> ```