Let's build a form validating mechanism in React with React-Hook-Form, Zod and TypeScript. The use case is form to add an User, with different types of inputs : string, email, url, date, email.
More advanced example:
Create a new project with Vite, React, Typescript, Zod and React Hook Form
pnpm create vite my-form-app --template react-ts
cd my-form-app
pnpm i
pnpm install react-hook-form zod @hookform/resolvers
Don't repeat the data structure: use a Zod Schema and infer it. It avoids the need for an explicit type alias, it is more concise.
// File: types.ts
import { z } from 'zod';
export const userSchema = z.object({
firstName: z.string().min(2).max(30),
});
export type UserType = z.infer<typeof userSchema>;Using typeof ensures that the type is determined at compile-time and is less likely to result in type errors or unexpected behavior.
Ref: useForm
It is a hook from React-Hook-Form. It takes on object as optional argument. It returns object that we can destructure.
// File: Form.tsx
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
// without ts : const { register } = useForm();
// with ts : const { register } = useForm<FormType>()
// with ts and zod
const { register } = useForm<UserType>(
resolver: zodResolver(userSchema),
)Ref: register
This method allows to register an element and apply validation rules to React Hook Form. When invoking register, I will receive following methods : name, onChange, onBlur, ref
// File: Form.tsx
<input type="text" {...register('firstName')} />
// Output: {name: 'firstName', onChange: ƒ, onBlur: ƒ, ref: ƒ}For number inputs, we have to specify that it is a number. If not, we get this error:
[!error] "Expected number, received string"
It is caused by the fact that the register function provided by the react-hook-form library returns a string value. By default, the ref property returns the value of the input as a string, even if the input type is set to "number".
This is a limitation of the HTML specification, which defines the input value as a string.
To solve this issue, we have to add valueAsNumber: true in register.
<input type="number"
{...register('zipCode', { valueAsNumber: true })} />valueAsNumber is a property of the HTMLInputElement interface, which represents an input element in an HTML document.
Ref: handleSubmit
// File: Form.tsx
const { register, handleSubmit } = useForm<UserType>({
resolver: zodResolver(userSchema),
});
// or
// const onSubmit: SubmitHandler<FormType> = (data)=>{
const onSubmit = (data: UserType) => {
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input type="text" {...register('firstName')} />
</form>
)Ref: formState
Track the user's interaction. It returns an object with : errors, isDirty, isSubmitting, touchedFields, submitCount, ...
// File: Form.tsx
const { register, handleSubmit, formState: {errors} } = useForm<UserType>({
resolver: zodResolver(userSchema),
});
// ...
return (
//...
<p>{errors.firstName?.message}</p>
)Here a minimal form with one string field: firstName
// File: Form.tsx
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
// Schema validation with Zod
const formSchema = z.object({
firstName: z.string().min(2).max(30),
});
// Typescript
type FormType = z.infer<typeof formSchema>;
// Form component
const Form = () => {
// Get register, handleSubmit and errors
// + use a resolver to use Zod
const {
register,
handleSubmit,
formState: { errors },
} = useForm<FormType>({
resolver: zodResolver(formSchema),
});
// Get data after validation
const onSubmit = (data: FormType) => {
console.log(data);
};
// Handle submit form
// + register the input element
// + display error messages
return (
<form onSubmit={handleSubmit(onSubmit)}>
<label>
First name
<input type="text" {...register('firstName')} />
</label>
{errors.firstName &&
<p role="alert">{errors.firstName?.message}</p>}
<input type="submit" />
</form>
);
};
export default Form;