Your First Form
Let's build a real form. By the end of this lesson you'll have a working form with two fields and a submit button — all wired up with React Hook Form in just a few lines of code.
Setting Up useForm
Everything starts with the useForm hook. Call it at the top of your component and pass in a defaultValues object. The defaultValues define the shape of your form data and provide the initial value for each field.
import { useForm } from "react-hook-form";
function App() {
const { register, handleSubmit } = useForm({
defaultValues: {
firstName: "",
lastName: "",
},
});
}
Here we're telling React Hook Form that our form has two fields — firstName and lastName — and both start as empty strings. The hook returns several helpers; for now we only need register and handleSubmit.
Wiring Up the Form Element
Next, return a <form> element and connect handleSubmit to its onSubmit event. The handleSubmit function takes your own callback, wraps it, prevents the default browser submission, runs any validation you've configured, and — if everything passes — calls your callback with the form data as a typed object.
function App() {
const { register, handleSubmit } = useForm({
defaultValues: {
firstName: "",
lastName: "",
},
});
const onSubmit = (data) => {
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
{/* fields will go here */}
<button type="submit">Submit</button>
</form>
);
}
Notice that onSubmit receives the complete form data as a plain object. When the user submits, you'll see something like { firstName: "Jane", lastName: "Doe" } logged to the console. You never have to manually read values from the DOM.
Adding Your First Field
Now let's add a field. The register function is how you connect an <input> to React Hook Form. Call register with the field name and spread the result onto the input.
<input {...register("firstName")} />
That single line does a lot. register("firstName") returns an object containing { onChange, onBlur, ref, name }. When you spread it onto the input, React Hook Form automatically tracks every change and blur event without you writing any event handlers. The ref gives the library direct access to the DOM node, which is how it reads values without re-rendering your component on every keystroke.
Adding a Second Field
Adding another field follows the exact same pattern. Just call register with the matching key from defaultValues.
<input {...register("lastName")} />
That's it. Each field is independently tracked using the name you pass to register. The name must match a key in your defaultValues object so that React Hook Form knows which piece of state to update.
The Complete Form
Here's everything put together:
import { useForm } from "react-hook-form";
function App() {
const { register, handleSubmit } = useForm({
defaultValues: {
firstName: "",
lastName: "",
},
});
const onSubmit = (data) => {
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label htmlFor="firstName">First Name</label>
<input id="firstName" {...register("firstName")} />
</div>
<div>
<label htmlFor="lastName">Last Name</label>
<input id="lastName" {...register("lastName")} />
</div>
<button type="submit">Submit</button>
</form>
);
}
export default App;
Type into both fields, click Submit, and open the console. You'll see a clean object with your values — no useState, no onChange handlers, no manual wiring.
The Three Core Pieces
Every React Hook Form setup uses the same three building blocks:
useForm— initializes the form and returns helpers. ThedefaultValuesoption defines your fields and their starting values.register— connects an input to the form. It returnsonChange,onBlur,ref, andnameso the library can track the field without you writing boilerplate.handleSubmit— wraps your callback. It prevents the default form submission, runs validation, and passes the collected data to your function.
These three pieces are the foundation of every React Hook Form implementation. No matter how complex a form gets, it always starts here.
Try It Yourself
To practice, try adding an email field to the form. Add email: "" to defaultValues, register it with register("email"), and give it a label. In the next lesson we'll take a closer look at everything register returns and how to customize its behavior.