How to use debounce on input change with React-hook-form library

August 16th, 2020

Recently, I've been using react-hook-form to create a login form. The library has a lot of examples on the github page. However, I could not find a case for my use. I wanted to use debounce on inputChange to avoid trigger validation on every keystroke from users. It took me a couple of days to get the result. Therefore, I decided to write this blog to share with anyone who wants to implement the same behavior

The version of react-form-hook mentioned in this blog is version 6.

Basic use of react-hook-form

The code below shows you the basic usage. The code is from here.

import React from 'react'
import { useForm } from 'react-hook-form'

const Example = () => {
  const { handleSubmit, register, errors } = useForm()  const onSubmit = values => console.log(values)
  return (
    <form onSubmit={handleSubmit(onSubmit)}>      <input
        name="email"
        ref={register({          required: 'Required',          pattern: {            value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,            message: 'invalid email address',          },        })}      />
      {errors.email && <p>{errors.email.message}</p>}

      <input
        name="username"
        ref={register({          validate: (value) => value.length > 5        })}      />
      {errors.username && errors.username.message}

      <button type="submit">Submit</button>
    </form>
  )
}

Adding onChange handler for input fields

import React from 'react'
import { useForm } from 'react-hook-form'

const Example = () => {
  const { handleSubmit, register, errors } = useForm()
  const onSubmit = values => console.log(values)

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input
        name="email"
        ref={register({
          required: 'Required',
          pattern: {
            value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
            message: 'invalid email address',
          },
        })}
        onChange={() => console.log('email input changed')}      />
      {errors.email && <p>{errors.email.message}</p>}

      <input
        name="username"
        ref={register({
          validate: (value) => value.length > 5
        })}
        onChange={() => console.log('user input changed')}      />
      {errors.username && errors.username.message}

      <button type="submit">Submit</button>
    </form>
  )
}
spinning-sun

At this point, using debounce to only fire the callback after a certain amount of time is one of the good ways to improve client-side performance. You can write a simple function to use, or you can install a small debounce package which I prefer. This function will take a function as the first argument, and a wait time as the second argument.

import React from 'react'
import { useForm } from 'react-hook-form'
import debounce from 'debounce'
const Example = () => {
  const { handleSubmit, register, errors } = useForm()
  const onSubmit = values => console.log(values)

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input
        name="email"
        ref={register({
          required: 'Required',
          pattern: {
            value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
            message: 'invalid email address',
          },
        })}
        onChange={debounce(() => console.log('email input changed'), 500)}      />
      {errors.email && <p>{errors.email.message}</p>}

      <input
        name="username"
        ref={register({
          validate: (value) => value.length > 5
        })}
        onChange={debounce(() => console.log('user input changed'), 500)}      />
      {errors.username && errors.username.message}

      <button type="submit">Submit</button>
    </form>
  )
}

Now, the callback only triggers when users stop typing more than 500ms

spinning-sun

Trigger validation

By default, react-form-hook will trigger validation automatically based on our choice of mode, but the library also exposes trigger API where you can trigger the validation manually. In our case, that is what we are going to use in our onChange handlers. Since the function returns a Promise, we need to use async/await for the callback functions.

import React from 'react'
import { useForm } from 'react-hook-form'
import debounce from 'debounce'
const Example = () => {
  const { handleSubmit, register, errors, trigger } = useForm()
  const onSubmit = values => console.log(values)

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input
        name="email"
        ref={register({
          required: 'Required',
          pattern: {
            value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
            message: 'invalid email address',
          },
        })}
        onChange={debounce(async () => {          await trigger('email')        }, 500)}      />
      {errors.email && <p>{errors.email.message}</p>}

      <input
        name="username"
        ref={register({
          validate: (value) => value.length > 5
        })}
        onChange={debounce(async () => {          await trigger('username')        }, 500)}      />
      {errors.username && errors.username.message}

      <button type="submit">Submit</button>
    </form>
  )
}

Why don't we useonChange mode in useForm?

react-hook-form has the onChange mode which will automatically trigger validation when there is a change in the input fields. For some reason, it only triggers the debounce function, but the validation won't work. I've tried that approach when I wanted to have the debounce effect on inputs, but it does not trigger the validation as I thought it would be. The function we passed to the debounce would trigger, but the validation functions did not work. I've gone down that path for a few days before I took another approach with the manual trigger API.

Bonus: How to disable the submit button when there are one or more invalid fields

We need to have an object state to keep track of whether each field is valid or not, and then we have a variable to keep an eye on all the fields
To know if all properties in an object are truthy, we can use javascript Object.keys and every.

The Object.keys will return an array of the object values and every will return true if each element in the array passes the callback function. That means that if one or more fields in the isValid object are false, isFormValid will be false.

In our example, we have username and email.

const [isValid, setIsValid] = React.useState({ email: false, username: false })

const isFormValid = Object.values(isValid).every(val => val)

The submit button will look like below:

<button type="submit" disabled={!isFormValid}>
  Submit
</button>

so now, the submit button will have a disabled state depending on the isFormValid. The next step is to know when to update the state.

If you read the document carefully about the trigger API on their website, you will notice that it returns a Promise<boolean>, which means eventually it will tell you whether the field is valid or not. We can make use of that and update the isValid object state right after we trigger the validations.

// Some lines are omitted
const Example = () => {
  return (
    <form>
      <input
        name="email"
        onChange={debounce(async () => {
          const result = await trigger('email')
          setIsValid(prevState => ({ ...prevState, email: result }))        }, 500)}
      />
      <input
        name="username"
        onChange={debounce(async () => {
          const result = await trigger('username')
          setIsValid(prevState => ({ ...prevState, username: result }))        }, 500)}
      />
    </form>
  )
}

Final code sample


Tagged in frontendjavascripttutorial

Share