DEV Community

Abhishek Gupta
Abhishek Gupta

Posted on

πŸš€ React Hook Form: From Beginner to Monster β€” Everything You Need to Know

I spent several days debugging React Hook Form, understanding why some components rerender while others don't, learning about subscriptions, Proxies, Controllers, FormProvider, useFormState, and the internals that make RHF one of the fastest form libraries in the React ecosystem.

This article contains everything I wish someone had explained when I started.

If you understand all of these concepts, you'll not only become productive with RHF but also gain a deeper understanding of React rendering, subscriptions, and state management.

Let's begin.


Why React Hook Form Exists

Most React developers start with this approach:

const [name, setName] = useState("");

<input
    value={name}
    onChange={(e) => setName(e.target.value)}
/>
Enter fullscreen mode Exit fullscreen mode

This works fine for a few fields.

But imagine building:

  • Login Form
  • Signup Form
  • Admin Dashboard
  • Course Creation Page
  • E-commerce Product Form
  • Job Portal
  • CMS
  • Dynamic Forms

Suddenly you have:

const [name,setName]
const [email,setEmail]
const [password,setPassword]
const [errors,setErrors]
const [isDirty,setDirty]
const [touched,setTouched]
Enter fullscreen mode Exit fullscreen mode

The code becomes messy.

Every keystroke rerenders components.

Validation logic becomes difficult.

Performance suffers.

React Hook Form solves these problems.


Why React Hook Form is Fast

Most libraries use:

useState()
Enter fullscreen mode Exit fullscreen mode

RHF uses:

Uncontrolled Components

Instead of storing every keystroke inside React State,

RHF stores values inside refs.

This means:

Typing:

a
ab
abc
abcd
Enter fullscreen mode Exit fullscreen mode

does NOT rerender the entire component tree.

This is the secret sauce.


useForm()

The most important hook.

const methods = useForm();
Enter fullscreen mode Exit fullscreen mode

It returns:

register
handleSubmit
watch
control
formState
reset
setValue
getValues
trigger
setError
clearErrors
resetField
setFocus
Enter fullscreen mode Exit fullscreen mode

register()

Simplest way to connect an input.

<input

{...register("name")}

/>
Enter fullscreen mode Exit fullscreen mode

RHF internally adds:

value

onChange

onBlur

ref
Enter fullscreen mode Exit fullscreen mode

Automatically.


Validation

Simple validation

register(

"name",

{

required:true

}

)
Enter fullscreen mode Exit fullscreen mode

Advanced

register(

"name",

{

required:"Name required",

minLength:{

value:3,

message:"Minimum 3"

}

}

)
Enter fullscreen mode Exit fullscreen mode

Zod Integration

Install

npm i zod
npm i @hookform/resolvers
Enter fullscreen mode Exit fullscreen mode

Schema

const Schema=z.object({

name:

z.string()

.min(

1,

"Required"

)

});
Enter fullscreen mode Exit fullscreen mode

Hook

useForm({

resolver:

zodResolver(

Schema

)

})
Enter fullscreen mode Exit fullscreen mode

handleSubmit()

const submit=(data)=>{

console.log(data);

};



<form

onSubmit={

handleSubmit(

submit

)

}

>
Enter fullscreen mode Exit fullscreen mode

Error callback

const onError=(errors)=>{

console.log(errors);

};



handleSubmit(

submit,

onError

);
Enter fullscreen mode Exit fullscreen mode

watch()

Subscribe to field values.

watch("email");
Enter fullscreen mode Exit fullscreen mode

Example

const title=

watch(

"courseName"

);
Enter fullscreen mode Exit fullscreen mode

When title changes,

Component rerenders.


useWatch()

Optimized version.

const title=useWatch({

control,

name:"courseName"

});
Enter fullscreen mode Exit fullscreen mode

Only rerenders for this field.

Best for large applications.


getValues()

Read values.

No rerender.

getValues();
Enter fullscreen mode Exit fullscreen mode

Single

getValues(

"email"

);
Enter fullscreen mode Exit fullscreen mode

setValue()

Programmatically update.

setValue(

"name",

"Abhishek"

);
Enter fullscreen mode Exit fullscreen mode

Options

setValue(

"name",

"John",

{

shouldValidate:true,

shouldDirty:true,

shouldTouch:true

}

);
Enter fullscreen mode Exit fullscreen mode

reset()

reset();
Enter fullscreen mode Exit fullscreen mode

Default

reset({

name:"John"

});
Enter fullscreen mode Exit fullscreen mode

trigger()

Validate manually.

await trigger();
Enter fullscreen mode Exit fullscreen mode

Specific

await trigger(

"email"

);
Enter fullscreen mode Exit fullscreen mode

setError()

Custom validation.

setError(

"email",

{

message:"Email exists"

}

);
Enter fullscreen mode Exit fullscreen mode

clearErrors()

clearErrors();
Enter fullscreen mode Exit fullscreen mode

Specific

clearErrors(

"email"

);
Enter fullscreen mode Exit fullscreen mode

Controller()

For custom components.

Bad

<Input

value={watch()}

onChange={()=>setValue()}

/>
Enter fullscreen mode Exit fullscreen mode

Good

<Controller

name="title"

control={control}

render(

{

field,

fieldState

}

)=>(


<Input


value={field.value}

onChange={field.onChange}

error={fieldState.error?.message}


/>

)

}

/>
Enter fullscreen mode Exit fullscreen mode

fieldState

Inside Controller

fieldState.error


fieldState.invalid


fieldState.isTouched
Enter fullscreen mode Exit fullscreen mode

Automatic subscription.


useFieldArray()

Dynamic forms.

Perfect for:

Skills

Projects

Education

Requirements

Admission Criteria

Example

const{

fields,

append,

remove

}=

useFieldArray({

control,

name:"skills"

});
Enter fullscreen mode Exit fullscreen mode

Add

append({

value:""

});
Enter fullscreen mode Exit fullscreen mode

Delete

remove(index);
Enter fullscreen mode Exit fullscreen mode

FormProvider

Best architecture.

Wrapper

<FormProvider

{...methods}

>

<App/>

</FormProvider>
Enter fullscreen mode Exit fullscreen mode

Consume

const methods=

useFormContext();
Enter fullscreen mode Exit fullscreen mode

The Biggest Confusion: formState

Everyone writes:

const{

formState:{

errors

}

}=methods;
Enter fullscreen mode Exit fullscreen mode

Sometimes

Errors update.

Sometimes

Component doesn't rerender.

Why?


formState is NOT Normal State

It's a

Proxy.

Something similar to:

new Proxy(

state,

{

get(){

subscribe();

}

}

)
Enter fullscreen mode Exit fullscreen mode

RHF only rerenders subscribers.


Why My Component Didn't Rerender?

I had:

const methods=

useCourseFormContext();



console.log(

methods.formState.errors

);
Enter fullscreen mode Exit fullscreen mode

Validation happened.

Errors existed.

But component never rerendered.


Solution

Subscribe.

const{

errors

}=

useFormState({

control

});
Enter fullscreen mode Exit fullscreen mode

Now

Errors change

↓

Component rerenders

Instantly.


useFormState()

Perfect for:

errors


isDirty


isValid


dirtyFields


touchedFields


submitCount
Enter fullscreen mode Exit fullscreen mode

Example

const{

errors,

isDirty,

isValid

}=

useFormState({

control

});
Enter fullscreen mode Exit fullscreen mode

Validation Modes

onSubmit

mode:"onSubmit"
Enter fullscreen mode Exit fullscreen mode

Only submit.


onChange

mode:"onChange"
Enter fullscreen mode Exit fullscreen mode

Validate every key.


onBlur

mode:"onBlur"
Enter fullscreen mode Exit fullscreen mode

Validate after blur.


all

mode:"all"
Enter fullscreen mode Exit fullscreen mode

Everything.


Performance Tips

Use

βœ… register

βœ… Controller

βœ… fieldState

βœ… useWatch

βœ… useFormState

βœ… useFieldArray

βœ… FormProvider

Avoid

❌ watch everywhere

❌ setValue everywhere

❌ Multiple useForm()

❌ Manual states

❌ Copying RHF data into useState


Interview Questions

Why is RHF fast?

Because it uses uncontrolled components, refs, subscriptions, and Proxy-based formState instead of storing every field in React state.


Difference between watch and useWatch?

watch rerenders the parent component.

useWatch subscribes only to selected fields.


Difference between Controller and register?

register works for native inputs.

Controller is designed for custom controlled components.


Why useFormState?

To subscribe components to errors, dirty state, touched fields, and validity changes.


Why FormProvider?

To avoid prop drilling and share the same form instance across deeply nested components.


Final Thoughts

React Hook Form isn't just another form library.

It's a carefully designed subscription-based state management system optimized specifically for forms.

Top comments (0)