React Hook Form
General Usage
Based on the documentation at React Hook Form.
React Hook Form’s design and philosophy focus on user and developer experience. The library aims to provide users with a smoother interaction experience by fine-tuning the performance and improving accesibility. Some of the performance enhancements include:
-
Form state subscription model through the proxy
-
Avoiding unnecessary computation
-
Isolating component re-rendering when required
It aims at improving the user experience while users interact with the application . As for the developers, there’s built-in validation closely aliged with HTML standards allowing further extension with powerful validation methods and integration with schema validation natively. It also has type-checked forms with the help of typescript to help and guide the developer to build a robust form solution.
Let’s look at a bit of code first and then start breaking it down alongside the usage of the different utilities the library provides:
import { SubmitHandler, useForm } from 'react-hook-form'
interface Inputs { (1)
example: string
exampleRequired: string
}
function ReactHookFormDemo () {
const {
register, (3)
handleSubmit, (4)
watch,
formState: { errors }
} = useForm<Inputs>() (2)
const onSubmit: SubmitHandler<Inputs> = console.log
console.log(watch('example')) (5)
return (
/* "handleSubmit" will validate your inputs before invoking "onSubmit" */
<form onSubmit={handleSubmit(onSubmit)}>
<h1>React Hook Form</h1>
{/* register your input into the hook by invoking the "register" function */}
<input defaultValue='test' {...register('example')} />
{/* include validation with required or other standard HTML validation rules */}
<input {...register('exampleRequired', { required: true })} /> (6)
{/* errors will return when field validation fails */}
{errors.exampleRequired && <span>This field is required</span>}
<input type='submit' />
</form>
)
}
export { ReactHookFormDemo }
1 | The docs example makes usage of type , both interface and type function
for specific use cases, but in most common ones they are interchangable. Now,
that doesn’t mean you should’t have a clear defined coding style. |
2 | This hook will take care of exposing to us the different functions and properties that React Hook Form uses. |
3 | register is the function in charge of registering individual controls into the
hook’s state. It will inject into the control all the things neccesary to hook it up
to the form’s validations, errors, state changes and so on. |
4 | As an intermediary function we have handleSubmit that takes care of running validations
against the state before trying to execute onSubmit which is the function to react
to the form’s submission. |
5 | One clear example of how React Hook Form leverages typescript to the fullest, is
that in functions such as watch it will detect the actual valid values based on the
shape that we provided the form. |
6 | And as you can see the way to inject validations can easily be done through an
a second argument object provided to the register function. |
When it comes to validations its API is quite simple and extendable nevertheless:
import { SubmitHandler, useForm } from 'react-hook-form'
interface Inputs {
example: string
exampleRequired: string
}
function ReactHookFormDemo () {
const {
register,
handleSubmit,
watch,
formState: { errors }
} = = useForm<Inputs>({
defaultValues: { (5)
example: 'defaultExample',
exampleRequired: 'exampleRequired'
}
})
const onSubmit: SubmitHandler<Inputs> = console.log
console.log(errors) (1)
console.log('watch', watch('example')) (7)
return (
/* "handleSubmit" will validate your inputs before invoking "onSubmit" */
<form onSubmit={handleSubmit(onSubmit)}>
<h1>React Hook Form</h1>
{/* register your input into the hook by invoking the "register" function */}
<input defaultValue='test' {...register('example')} />
{/* include validation with required or other standard HTML validation rules */}
<input
{...register('exampleRequired', { (6)
required: 'This is required', (2)
minLength: { value: 4, message: 'Min length is 4' } (3)
})}
/>
{/* errors will return when field validation fails */}
{errors.exampleRequired && <span>{errors.exampleRequired.message}</span>} (4)
<input type='submit' />
</form>
)
}
export { ReactHookFormDemo }
1 | We can also watch all the errors through the errors property the hook returns,
and this is a simple object with all errors associated with the registered fields of
the form. |
2 | We can just put a validation value in a key-value pair fashion <string, boolean>,
however if we want to customize the error message we can provide instead of a boolean
a string. |
3 | For validations that require extra inputs we can provide an object that will hold the neccesary parameters plus the error message. |
4 | We will have the error object if it has errors with the name of the field and then
inside of it a message property alongside other meta-fashion data. |
5 | We can easily provide initialValues to the hook so that the form’s state is
initialized respectively. |
6 | And due to the hard typescript integration of the library, if we try to register a field with a name that’s not part of our type/interface we will get a linter error. |
7 | And one last bit of info, this watch function is great if we want to react to
changes produced on a specific form field, if we don’t provide a name we will subscribe
to the entire form’s state change. However if we have specific cases, we can carve them
out and apply logic that way. This will trigger only a re-render on specific fields and
won’t have to run the whole form rendering. |
And, we can rest assured that the way the form re-renders is optimal, it doesn’t render the whole thing, it follows React principles in order to render itself correctly.
Validation
React Hook Form makes form validation easy by aligning with the existing HTML standard for form validation
The list of supported validation rules are:
-
required
-
min
-
max
-
minLength
-
maxLength
-
pattern
-
validate
validate
is pretty interesting since you can feed your custom validation logic to it:
import React from "react";
import { useForm } from "react-hook-form";
function App() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm();
const onSubmit = (data) => {
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
{/* Password Field */}
<input
type="password"
placeholder="Password"
{...register("password", {
required: "Password is required",
validate: { (1)
minLength: (value) => (2)
value.length >= 8 || "Password must be at least 8 characters", (3)
containsNumber: (value) =>
/\d/.test(value) || "Password must contain a number",
},
})}
/>
{errors.password && <p>{errors.password.message}</p>}
<button type="submit">Submit</button>
</form>
);
}
export default App;
1 | Passing down to a validate property we can feed an object. |
2 | This object can then have different validation error names, this should always follow the key-value pair pattern, in a <string, Function> manner. |
3 | A pretty concise way of returning the error, in case the check passes we return a
boolean (true), if not, due to or we will return the error message. |
Besides feeding custom validations, if we need to account for more complex cases such
as two fields being validated between one and other, we can make use of the submit
function we ourselves feed into handleSubmit
:
import React from "react";
import { useForm } from "react-hook-form";
function App() {
const {
register,
handleSubmit,
watch,
setError, (1)
clearErrors, (2)
formState: { errors },
} = useForm();
const onSubmit = (data) => {
if (data.password !== data.confirmPassword) {
setError("confirmPassword", { (3)
type: "manual", (4)
message: "Passwords do not match",
});
} else {
console.log(data);
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
{/* Password Field */}
<input
type="password"
placeholder="Password"
{...register("password", {
required: "Password is required",
})}
/>
{errors.password && <p>{errors.password.message}</p>}
{/* Confirm Password Field */}
<input
type="password"
placeholder="Confirm Password"
{...register("confirmPassword", {
required: "Confirm Password is required",
})}
/>
{errors.confirmPassword && <p>{errors.confirmPassword.message}</p>}
<button type="submit">Submit</button>
</form>
);
}
export default App;
1 | We can leverage setErrors from the hook itself to raise an error manually. |
2 | We can then leverage clearErrors in case we have to manually get rid of an error,
this is only necessary in case we register a custom error for the form, not associated
with any registered input. Otherwise it will never go away. |
3 | We can register for a specific field a custom error manual in this case, and
associated with it we can then put the message . Notice how we are doing the validation
at the onSubmit function, it’s there that if we find an issue we leverage setErrors . |
However, using schema validations with stuff like Yup
or Zod
is a much cleaner
and scalable approach:
First, you have to install the resolver for the respective schema validator (in our case
we will leverage yup): pnpm i @hookform/resolvers yup
.
import { yupResolver } from '@hookform/resolvers/yup'
import { SubmitHandler, useForm } from 'react-hook-form'
import * as yup from 'yup'
interface Inputs {
example: string
exampleRequired: number
}
const schema = yup.object({ (1)
example: yup.string().required('Required'),
exampleRequired: yup
.number()
.typeError('Must be a number') (3)
.positive()
.integer()
.required('Required')
})
function ReactHookFormDemo () {
const {
register,
handleSubmit,
watch,
formState: { errors }
} = useForm<Inputs>({
defaultValues: {
example: 'defaultExample',
exampleRequired: 123
},
resolver: yupResolver(schema) (2)
})
const onSubmit: SubmitHandler<Inputs> = console.log
console.log(errors)
console.log('watch', watch('example'))
return (
/* "handleSubmit" will validate your inputs before invoking "onSubmit" */
<form onSubmit={handleSubmit(onSubmit)}>
<h1>React Hook Form</h1>
{/* register your input into the hook by invoking the "register" function */}
<input defaultValue='test' {...register('example')} />
{/* include validation with required or other standard HTML validation rules */}
<input {...register('exampleRequired')} /> (4)
{/* errors will return when field validation fails */}
{errors.exampleRequired && <span>{errors.exampleRequired.message}</span>}
<input type='submit' />
</form>
)
}
export { ReactHookFormDemo }
1 | We can create the schema through yup’s functions and have it stored in a variable. |
2 | We can then feed into the resolver property of the hook our schema. |
3 | One small comment when it comes to yup’s API, when enforcing a type you might get
a really verbose error, you can use typeError right after the type validator in
order to customize the message (you can’t just feed a parameter unfortunately). |
4 | And we can then get rid of the validation object inside of register since we have
offloaded that concern to the schema validator.
Another really interesting feature of React Hook Form is the fact that if a field has an error, it will immediately focus that field which has an error. |
Integrating with custom and library components
Existing Custom Form
If you have an existing form we can integrate seamlessly with the correct functions and signatures:
import { Path, useForm, UseFormRegister, SubmitHandler } from "react-hook-form"
interface IFormValues {
"First Name": string (1)
Age: number
}
type InputProps = {
label: Path<IFormValues> (2)
register: UseFormRegister<IFormValues> (3)
required: boolean
}
// The following component is an example of your existing Input Component
const Input = ({ label, register, required }: InputProps) => (
<>
<label>{label}</label>
<input {...register(label, { required })} /> (4)
</>
)
// you can use React.forwardRef to pass the ref too
const Select = React.forwardRef< (5)
HTMLSelectElement, (11)
{ label: string } & ReturnType<UseFormRegister<IFormValues>> (6)
>(({ onChange, onBlur, name, label }, ref) => (
<>
<label>{label}</label>
<select name={name} ref={ref} onChange={onChange} onBlur={onBlur}> (7)
<option value="20">20</option>
<option value="30">30</option>
</select>
</>
))
const App = () => {
const { register, handleSubmit } = useForm<IFormValues>() (8)
const onSubmit: SubmitHandler<IFormValues> = (data) => {
alert(JSON.stringify(data))
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Input label="First Name" register={register} required /> (9)
<Select label="Age" {...register("Age")} /> (10)
<input type="submit" />
</form>
)
}
1 | You can technically declare property names with spaces if you enclose the name with quotes (but this is not really recommended). |
2 | Path is a utility type that ensures that the value under this property should
correspond to one of the keys from IFormValues . This prevents typos and ensures
type safety when referencing form fields. |
3 | register is the function provided by useForm() to register inputs with React
Hook form, UseFormRegister ensures that the register function is correctly typed
for the fields in IFormValues . And so, when trying to call register("First Name")
in this inner component, Typescript will prevent invalid field names. |
4 | As you can see we are basically passing the register function in order to then
call it in the inner component’s rendering next to the input component. |
5 | This wrapper allows a component to receive a ref and pass it to a child element. Because we are
spreading the register() upstream, we would have { ref, name, onChange, onBlur }
to be sent to the component. However React, by design, doesn’t allow for refs to be passed down
directly, unless we are explicit about it. Hence we have to use forwardRef to state
our intentions clear, it’s with forms that we normally would make use of this resource. |
6 | ReturnType is a utility function that allows for us to easily resolve at runtime
the type for a specific field by simply feeding the signature of a function or other
type of member to it. In this instance we are simply trying to get a type for the return
of what would be a register for the IFormValues shape. |
7 | As you can see we are literally assigning each property that comes from up-stream
to the inner select . This ties into how we are registering the component at <10> |
8 | Never forget to type the useForm that’s how you unlock intellisense and typescript’s
magic. |
9 | See how we simply pass down the register from the hook, and we also declare
the required prop right after it. (At this level the register is but another property
that we leverage inside the component). |
10 | Another way to code the wiring to React Hook Form, we can simply spread the register function in the custom component itself, it’s downstream that we have coded everything so that it wires up correctly. |
11 | And so we also have to type the type of ref that the child component will receive,
in our case it should be an HTMLSelect , that’s what will be saved in the ref
parameter of the function. That way we can connect our select with the ref that
the register function will attempt at resolving. It’s all about being organized,
explicit, and knowing how everything interacts.
It’s up to you which approach to use, but again, you should have a clear code guideline set and you should stick to it. |
Third-party Forms
You should be able to integrate with UI component libraries just fine, if by any
chance the component does not expose the ref
you can leverage the Controller
input, he will take care of the registration process.
import Select from "react-select"
import { useForm, Controller, SubmitHandler } from "react-hook-form"
import { Input } from "@material-ui/core"
interface IFormInput {
firstName: string
lastName: string
iceCreamType: { label: string; value: string }
}
const App = () => {
const { control, handleSubmit } = useForm({ (1)
defaultValues: {
firstName: "",
lastName: "",
iceCreamType: {},
},
})
const onSubmit: SubmitHandler<IFormInput> = (data) => {
console.log(data)
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Controller (2)
name="firstName"
control={control} (3)
render={({ field }) => <Input {...field} />} (4)
/>
<Controller
name="iceCreamType"
control={control}
render={({ field }) => (
<Select
{...field}
options={[
{ value: "chocolate", label: "Chocolate" },
{ value: "strawberry", label: "Strawberry" },
{ value: "vanilla", label: "Vanilla" },
]}
/>
)}
/>
<input type="submit" />
</form>
)
}
1 | The hook will return a control function that we can then leverage to hook into
library components. |
2 | We can use the Controller component to sort of act as a wrapper, he will take
care of registering the respective field on whatever input field our library component
encapsulates. |
3 | We have to feed the control property to the Controller component so that it
wires up correctly. |
4 | And lastly we have to send a render prop under render , this will take care of
rendering the library component and also hook into its input correctly.
You can use both the Component API and the Hook API however you see fit, again stick to one method. And most of the time it will be based around what is possible with the tools you get. If things work a certain way, and you can make your code explicit, yet clean enough for it to keep working. Follow that line. |