7 min read • Jan 31, 2025
React Hook Form
React Hook Form - the basics

There are two ways to create a form using React Hook Form. The first uses the register function, which relies on uncontrolled inputs, and the second uses the controller, which manages controlled inputs.
While React typically favors controlled components, React Hook Form’s best practice is to use uncontrolled components for better performance and fewer re-renders. Using a register is recommended if your form does not require immediate validation or dynamically rendered fields.
This article is a step-by-step guide to creating, validating, and adding your onSubmit function using the register method:
Here are all the steps:
Step 1: Install React Hook Form
Step 2: Import React Hook Form into a Component
Step 3: Connect Form Inputs to useForm
Step 4: Create a Custom onSubmit Function
Step 5: Add Field Validation
Step 1: Install it
npm install react-hook-formStep 2: Import
Let’s create a sign-in form component as an example and import useForm :
import { useForm } from "react-hook-form";
const SignInForm: React.FC = () => {
return (
...
);
};
export default SignInForm;Now, we can initialize useForm and keep returning value in a variable form. We will need two fields for this form, email and password, and we can define the types of these fields and pass them to useForm :
import { useForm } from "react-hook-form";
interface SignInFormFields {
email: string;
password: string;
}
const SignInForm: React.FC = () => {
const form = useForm<SignInFormFields>();
return (
...
);
};
export default SignInForm;The returned value (stored in form) contains several functions and properties we can use for building our form.
We’ll start with the register function, which is used to connect input elements to the form, enabling React Hook Form to track their values and validations.
Step 3: Register inputs
To directly access the register function, we can use the object destruction method:
import { useForm } from "react-hook-form";
interface SignInFormFields {
email: string;
password: string;
}
const SignInForm: React.FC = () => {
const { register } = useForm();
return (
...
);
};
export default SignInForm;Now, we can use this function to connect inputs to useForm. Using registerwill make input value available for both the form validation and submission.
Note: Each field is required to have a name as a key for the registration process.
import { useForm } from "react-hook-form";
interface SignInFormFields {
email: string;
password: string;
}
const SignInForm: React.FC = () => {
const { register } = useForm();
const email = register("email")
return (
...
);
};
export default SignInForm;By calling register(“email”), we’re telling React Hook Form to associate this input field with the name “email”.
The register is a callback function returns an object containing props — name, onChange, onBlur, and ref. We can inject these props into our input by spreading the returned object:
<input {...email} placeholder="Enter your email" />
// The above code is equivalent to writing:
<input
name="email"
onChange={(event) => { /* internal onChange logic */ }}
onBlur={(event) => { /* internal onBlur logic */ }}
ref={(element) => { /* attach to form */ }}
placeholder="Enter your email"
/>Then, we spread this returned object into the <input> element to connect it to the React Hook Form.
import { useForm } from "react-hook-form";
interface SignInFormFields {
email: string;
password: string;
}
const SignInForm: React.FC = () => {
const { register } = useForm();
const email = register("email");
return (
<form>
<input {...email} placeholder="Enter your email" />
</form>
);
};
export default SignInForm;I prefer assigning a returned value of a register to a variable to keep JSXsimpler. It is not required, though. You can directly spread it into an input:
<input {...register("email")} placeholder="Enter your email" />We can do the same for the password:
import { useForm } from "react-hook-form";
interface SignInFormFields {
email: string;
password: string;
}
const SignInForm: React.FC = () => {
const { register } = useForm<SignInFormFields>();
const email = register("email");
const password = register("password");
return (
<form>
<input {...email} placeholder="Enter your email" />
<input {...password} placeholder="Enter your password" />
</form>
);
};
export default SignInForm;Now, our input fields are registered to the form, meaning that when their values change, they will be sent to the React Hook Form.
Step 4: Next, we can define our onSubmit function:
To make React Hook Form manage our inputs for us, we will use the handleSubmit function and pass our onSubmit.
handleSubmit function wraps the submit handler and validates fields based on the provided rules. And only calls the submission callback if validation passes.
import { SubmitHandler, useForm } from "react-hook-form";
interface SignInFormFields {
email: string;
password: string;
}
const SignInForm: React.FC = () => {
const { register } = useForm<SignInFormFields>();
const email = register("email");
const password = register("password");
const onSubmit: SubmitHandler<SignInFormFields> = (data) => {
console.log(data)
}
return (
<form>
<input {...email} placeholder="Enter your email" />
<input {...password} placeholder="Enter your password" />
<button type="submit">Submit</button>
</form>
);
};
export default SignInForm;What this code does is that when a user submits the form, the form will call handleSubmit, which comes from a react hook form. This will prevent the form’s default behavior. Then, it will validate our inputs, and if validation is successful, it will call our onSubmit function.
Step 5: Validate fields
We can pass validation rules as an object to the register function’s second argument to validate user inputs. These rules define conditions the input must meet, like being required, having a minimum length, or matching a pattern. For example:
const email = register("email" , {
required: true,
});
const password = register("password", {
required: true,
minLength: 8,
});When the user submits the form, useForm checks whether the input data meets these rules (e.g., fields are filled in and the password is at least eight characters). If validation passes, the form will be sent to the server.
If validation fails, errors will be stored in the formState.errors object. Each field’s error can be accessed by its name, like errors.email or errors.password.
Currently, our code only checks whether a field has an error but doesn’t include error messages. To display messages for invalid fields, we can:
- Check for errors and show a custom message manually.
- Define error messages directly in the rules like this:
const email = register("email", { required: "This field is required" });
const password = register("password", {
required: "This field is required",
minLength: { value: 8, message: "Password must be at least 8 characters" }
});Now, if there are errors, we can show them directly in the UI
import { SubmitHandler, useForm } from "react-hook-form";
interface SignInFormFields {
email: string;
password: string;
}
const SignInForm: React.FC = () => {
const { register, handleSubmit, formState: {errors} } = useForm<SignInFormFields>();
const email = register("email");
const password = register("password");
const onSubmit: SubmitHandler<SignInFormFields> = (data) => {
console.log(data)
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...email} placeholder="Enter your email" />
{errors.email && <p>{errors.email.message}</p>}
<input {...password} placeholder="Enter your password" />
{errors.password && <p>{errors.password.message}</p>}
<button type="submit">Submit</button>
</form>
);
};
export default SignInForm;You can also use Zod for validation, which we will cover in the following article.
To sum up
We’ve created a simple form using useForm, where we:
- Used the
registerfunction to assign inputs to the form. - We created our
onSubmitfunction and passed it on to useForm’s handleSubmit function, which validates the input values before calling onSubmit. - Defined error messages, passed them to useForm, and displayed them in the UI.