Go to the home page logo

Why do you use react-hook-form?

You probably use react-hook-form or Formik for forms in React. Are you sure you need these libraries?

I think using react-hook-form makes sense only if:

  • built-in HTML validation is enough for you
  • the project doesn’t interact with an API or interacts exclusively through forms

In this case you can actually use react-hook-form to its full potential, because you don’t have to add schema validation (Zod, Valibot…) and data fetching (React Query, SWR…) libraries

react-hook-form provides <Form /> for conveniently sending data via forms, and also provides the ability to safely use browser-like validation. If you didn’t know, attributes like required, minLength, max, etc. can be easily removed via the developer console. react-hook-form performs validation locally

If you use schema validation and data fetching libraries, they replace you <Form /> and validation. There remain 2 useful features of react-hook-form that you can easily recreate yourself:

  • handleSubmit
  • watch

Let’s add types first. Parsed is unique for each validation library, I show an example with Zod. This is the hardest part:

type Parsed<T> =
  | z.SafeParseSuccess<T>
  | (Omit<z.SafeParseError<T>, "error"> & {
      error: z.typeToFlattenedError<T>["fieldErrors"];
    });
type FormState<T> = Parsed<T> | undefined;

Now let’s add two very simple functions:

function onSubmit<T>(
  e: FormEvent<HTMLFormElement>,
  schema: z.Schema<T>,
): Parsed<T> {
  e.preventDefault();
  const formData = new FormData(e.currentTarget);

  const result = schema.safeParse(Object.fromEntries(formData));
  if (!result.success) {
    return {
      ...result,
      error: result.error.flatten().fieldErrors,
    };
  }
  return result;
}

function useWatch<T>() {
  const [values, setValues] = useState<Partial<T>>({});

  function watch(e: ChangeEvent<HTMLInputElement>) {
    setValues({ ...values, [e.currentTarget.name]: e.currentTarget.value });
  }

  return [values, watch] as const;
}

I think I don’t even need to explain how these functions work. Here are docs for formData, Object.fromEntries(), Partial and Omit in case you don’t know about these. onSubmit() is also somewhat unique for each validation library, but you just need to apply validation

Usage example

import { ChangeEvent, FormEvent, useState } from "react";
import { z } from "zod";

export default function App() {
	const [result, setResult] = useState<FormState<LoginSchema>>();
	const handleSubmit = (parsed: Parsed<LoginSchema>) => setResult(parsed);

	const [values, watch] = useWatch<LoginSchema>();
	console.log(values.email);

	return (
		<form onSubmit={(e) => handleSubmit(onSubmit(e, LoginSchema))}>
			<input name="email" onChange={watch} />
			{result?.error?.email}
			<button>submit</button>
		</form>
	);
}

const LoginSchema = z.object({
	email: z
		.string({ message: "Your email must be a string." })
		.nonempty("Please enter your email.")
		.email("The email address is badly formatted."),
});

type LoginSchema = z.infer<typeof LoginSchema>;

The only downside is that checkboxes won’t send the correct data on submit by default, but this is an exercise for you if you want to use this