skip to content
Samuel Edwin's Website

Form Handling Without Suffering Using React Hook Form

/ 6 min read

Form is an important part of the web that allows users to submit data to the server.
It is the technology that allows us to book hotels for our next holiday or fill out our taxes.

Some industries such as banking, finance and taxes can have very complicated forms.

In this post I want to show you the problems handling forms with React and how React Hook Form solves this.

How hard is it to build a simple form in React?

Let me show you an example of a simple sign up form.

simple sign up form

Here are the required validations for this form:

  • Username must be between 3 to 8 characters, alphabets only.
  • Password must be between 5 to 10 characters.
  • Confirm Password must be the same as password.

How do you build this feature with React?

1. Define the components

Here’s a simplified version of how the components are defined.
Each text field has a label, the actual input element and also a div to show error message.

function App() {
return (
<form>
<label>Username</label>
<input />
<div className="error">Sample error</div>
<label>Password</label>
<input />
<div className="error">Sample error</div>
<label>Confirm Password</label>
<input />
<div className="error">Sample error</div>
<button type="submit">Register</button>
</form>
);
}

2. Handle input events

In order to get the user typed value, we can either:

  • useRef to capture the input ref
  • or useState to store the user input every time the value changes.

I’m gonna go with the latter approach.
We’re going to need three states.

function App() {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
return (
<form>
<label>Username</label>
<input />
<div className="error">Sample error</div>
{/* other components */}
</form>
);
}

Aside from creating states to store the input data, we also need states to store the error message.

function App() {
// ...
const [usernameError, setUsernameError] = useState("");
const [passwordError, setPasswordError] = useState("");
const [confirmPasswordError, setConfirmPasswordError] = useState("");
return (
// ...
<div className="error">{usernameError}</div>
// ...
);
}

And we’re going to assign the states to the input fields.

function App() {
const [username, setUsername] = useState("");
// rest of the states
return (
// ...
<input value={username} onInput={(e) => setUsername(e.currentTarget.value)} />
// do the same to other fields
);
}

3. Validating and submitting the form

This is how we validate the form data and then send it to the server.

function App() {
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setUsernameError("");
setPasswordError("");
setConfirmPasswordError("");
let hasError = false;
if (username.length < 3 || username.length > 8) {
setUsernameError("Username must be between 3 and 8 characters");
hasError = true;
} else if (!/^[a-zA-Z]+$/.test(username)) {
setUsernameError("Username must only contain alphabets");
hasError = true;
}
if (password.length < 5 || password.length > 10) {
setPasswordError("Password must be between 5 to 10 characters");
hasError = true;
}
if (password !== confirmPassword) {
setConfirmPasswordError("Passwords must match");
hasError = true;
}
if (!hasError) {
// submit the data to server
}
}
return <form onSubmit={handleSubmit}>{/* ... */}</form>;
}

You can see the full code here.

Why is it a problem?

This code is okay if you only deal with forms once or twice in your app, but can feel tedious very quickly if you have to deal with 20 forms, or maybe just one but with dozens of fields.

The problem can get worse when the requirements are more complex:

  • Users can add or remove up to 5 phone numbers.
  • The form is split into multiple steps.
  • Sometimes the form in the next steps become different depending on the user input.
  • Some sections become mandatory if the users tick some boxes in the immigration form.

How React Hook Form solves this

There are a lot of solutions from the React community to handle forms.
The best one I like so far is using react-hook-form to handle the events, and Zod to handle the validations.

Let me give you a quick tour of how it works by building the same feature.
Don’t worry I’ll explain more details in the future posts if you’re interested.

Define the validations

First we define the fields and their validations of our forms.

schema.ts
import z from "zod";
export const formSchema = z
.object({
username: z
.string()
.min(3, "Username must have at least 3 characters")
.max(8, "Username can't exceed 8 characters")
.regex(/^[A-Za-z]+$/, "Username can only contain alphabets"),
password: z
.string()
.min(5, "Password must be at least 5 characters long")
.max(10, "Password can't exceed 10 characters"),
confirmPassword: z.string(),
})
.refine((form) => form.password === form.confirmPassword, {
message: "Passwords don't match",
path: ["confirmPassword"],
});
export type RegistrationForm = z.TypeOf<typeof formSchema>;

You probably have an idea of what happened even if you don’t have any experience using Zod.

The .refine part is a bit advanced, but in short it is a way to do a custom validation for the fields.

Add validations to the form

The next thing is using the validations we defined previously to the form.

useForm will handle everything for us. All we need to do is to handle input events using register method.
You will receive the validated data in handleSubmit and send it to the server.

App.tsx
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { formSchema, RegistrationForm } from "./schema";
export default function App() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<RegistrationForm>({
resolver: zodResolver(formSchema),
});
return (
<form className="form" onSubmit={handleSubmit((data) => alert(JSON.stringify(data)))}>
<div className="textfield">
<label htmlFor="username">Username</label>
<input className="username" type="text" {...register("username")} />
<div className="error">{errors.username?.message}</div>
</div>
{/* Do the same thing to password and confirm password */}
<button type="submit">Register</button>
</form>
);
}

With just a few lines of code, React Hook Form helps us:

  • Handle user inputs
  • Validate and show appropriate error messages when the form is submitted
  • Gives us the validated data when the user inputs the valid data

I removed a lot of things for the sake of brevity.
You can take a look at the full code here

Why is this better?

Better is often subjective, but here are my personal reasons:

  • I don’t need to clutter the component with lots of states.
  • Lots of built in validations, I only need to describe what they are instead of writing lots of if statements.
  • Errors are handled automatically. You can choose whether to show errors when typing or clicking submit button.
  • Handles more advanced cases really well, which I will demonstrate in future posts.
  • Flexible options. You can choose whether to validate when the form is submitted, or when the fields are modified.

Conclusion

Manual form handling in React is tedious. react-hook-form solves this problem for us, allowing us to write less code.