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.