Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save jeromeabel/39bdc4fdfde32228483d3e9727a35cf3 to your computer and use it in GitHub Desktop.

Select an option

Save jeromeabel/39bdc4fdfde32228483d3e9727a35cf3 to your computer and use it in GitHub Desktop.

Create A React Form With React-Hook-Form & TypeScript - Guide

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:

Installation

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

Zod Schema & TypeScript

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.

useForm

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),
)

register

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: ƒ}

valueAsNumber

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.

handleSubmit

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>
)

formState : errors

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>
)

Example 1 : minimal form

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;

More Explanations

Zod & TS ?

Why we repeat the structure of types in TypeScript and Zod ?

  • TypeScript is a static type checker: checks the types at compile-time
  • Zod is a runtime schema validator: checks the data at runtime

Why Zod need TS ?

  • errors are caught at compile-time.
  • easy to understand and maintain

Use cases Zod :

  • données imprévisibles : input, API, ...

Zod & native form validation API ?

What happens on runtime when I use the standard constraint web api like type="email" and type checking of Zod z.string().email() ?

  • type="email" attribute provides immediate feedback to the user
  • z.string().email() : More robust validation; customizable error messages; consistency across different environments

React Hook Form & native form validation API ?

  • uses the default web constraint API of input fields : adds native attributes to input fields such as name, value, onChange, and onBlur.
  • adds custom validation rules and messages based on your validation schema
  • uses event listeners and e.preventDefault() to intercept the form submission and perform custom validation, and then calls your onSubmit function if there are no errors.

Advanced settings with Zod

link

By default, .url() matches with these url httpp:, http://e, http://e.ee. We can add more constraints with regexp:

link: z
	.string()
	.url() 
	.regex(
	/^(?:https?:\/\/)?(?:www\.)?[a-zA-Z0-9-]{2,}\.[a-zA-Z]{2,}(?:\/[^\s]*)?$/
	),

string

Add regex to constrain names fields only with letters, not numbers:

  firstName: z
    .string()
    .min(2)
    .max(50)
    .regex(/^[A-ÿ]{2,}[A-ÿ\-\s]*$/, {
      message: 'The first name must contain only letters',
    }),

date

Compute the age to handle minor, major validations:

  dateOfBirth: z.coerce
    .date()
    .min(new Date('1920-01-01'), {
      message: 'Date cannot go past January 1 1920',
    })
    .max(new Date(), { message: 'Date must be in the past' })
    .refine(
      (date) => {
        const ageDifMs = Date.now() - date.getTime();
        const ageDate = new Date(ageDifMs);
        const age = Math.abs(ageDate.getUTCFullYear() - 1970);
        return age >= 18;
      },
      { message: 'The employee must be 18 years or older' }
    ),
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment