Reactjs: Debounce forms

Do you really need a form library to build forms in React?

Hello friends, today I will continue my post on forms. This is the second post on the subject and I hope that it teaches you something new. In the last post I showed to you an example of controlled form implementation, and today I want to show you another example focused on performance as we develop a debounce form.

If you are interested in the last post, you can read it here

Controlled forms are the most popular form implementation and we can find it in many libraries, one example is Formik, however controlled form is not the only way that you can build forms with React, you can use an uncontrolled form, or debounce form. Here's a brief explanation about it:

  • Controlled form - controlled forms mean that every time an input event happens, the component that this state belongs will be rendered again.
  • Uncontrolled form - when we use uncontrolled forms we don't have any state to keep the input value, and we just take the input value when we want to use it.
  • Debounce form - debounce forms mix controlled and uncontrolled approaches. It's necessary to use a state to keep the input value, but this value is changed only after the last input event happens.

Debounce function forces a function to wait a certain amount of time before running again. The function is built to limit the number of times a function is called.

Let's start

First of all, you should clone the last example here

Creating the form component

We already have a form component, as we developed it in the last post, so we don't have to do a lot of things, let's do that:

Open the project that you already cloned and copy the folder ControlledForm and rename to DebounceForm, and import this new component to use inside the App.

function App() {
  return (
    <div className="container-fluid">
      <div className="row">
        <div className="col-lg-6 col-md-6">
          <DebounceForm />
        </div>
        <div className="col-lg-6 col-md-6">
          <FormControlled />
        </div>
      </div>
    </div>
  );
}

Debounce function

Debounce function is a higher-order function

But, what does that means?

Higher-order function is extensively used in javascript, you are probably using it even if you don't know it.

Higher-Order function is a function that receives a function as an argument or returns the function as output.

Okay, if you are ready we can start. The first thing that we should do is to create a function named debounce, this function will reduce the number of times that we change the form state and the number of renders of the component. Below, we can see my implementation:

export function debounce(fn, wait, immediate) {
  let timeout;

  return (...args) => {
    const context = this;

    const later = () => {
      timeout = null;
      if (!immediate) fn.apply(context, args);
    };

    const callNow = immediate && !timeout;
    clearTimeout(timeout);
    timeout = setTimeout(later, wait);

    if (callNow) {
      fn.apply(context, args);
    }
  };
}

So, a debounce function is a function that returns another function, and that returned function runs the function that we pass as a parameter into debounce function.

const callbackFunction = () => {}; // it will be executed into returned function
const time = 3000; // it's the delay time

const returnedFunction = debounce(callbackFunction, time);

returnedFunction(); // callbackFunction know my arguments

Using debounce function into Input component

I will use debounce function with onChange event from input, and add 500 milliseconds. This way the form state will only change after 500 milliseconds when the user stops writing.

import React, { useState, useRef, useEffect, useCallback } from "react";
import { debounce } from "../Debounce";

function Input({ error, label, onChange, ...rest }) {
  const [touched, setTouched] = useState(false);
  const inputRef = useRef(null);
  const debounceInput = useCallback(debounce(onChange, 500), [debounce]);
  const blurInput = useCallback(() => setTouched(true), [setTouched]);

  useEffect(() => {
    inputRef.current.addEventListener("input", debounceInput);
    inputRef.current.addEventListener("blur", blurInput);

    return () => {
      inputRef.current.removeEventListener("input", debounceInput);
      inputRef.current.removeEventListener("blur", blurInput);
    };
  }, [blurInput, debounceInput, inputRef]);

  return (
    <>
      <label htmlFor={rest.name}>{label}</label>
      <input className="form-control" {...rest} ref={inputRef} />
      <span className="text-danger">{touched && error}</span>
    </>
  );
}
export default Input;

Code explanation

The first thing that we should discuss is why I'm using useCallback. UseCallback is used when you want to memorize a function, this hook receives a function as an argument and memorizes it, and this hook will return the same function while the dependencies don't change. When some dependency is changed a new function is returned. But why do we need to do this? The functions inside a component will change every time that the component is rendered, so when I use useCallback I know that the function returned is the same, unless some dependency is changed.

The next thing we should understand is that:

A common mistake is to think functions shouldn’t be dependencies.

If a function is used inside a useEffect we should pass this function as a dependency, and we know that the function will change in every component render, for this reason, we use useCallback, if we don't, our component will be rendered unnecessary.

In the first part of our component code, we are using some hooks; useState to save blur event state, and useRef to create a reference to use in the input element. After that we use useCallback with debounce function and setTouched.

useEffect receives blurInput, debounceInput, inputRef as dependencies inside of the function that we use with useEffect. We use the input reference to register the functions to deal with input and blur events, after that, we just return a function that should remove the event listener functions.

Improving useValidation hook

useValidation is a hook that returns an object with errors and a property to show us if the form values are valid or not.

import { useState, useEffect, useCallback } from "react";
import { ValidationError } from "yup";

function useValidation(values, schema) {
  const [errors, setErrors] = useState({});
  const [isValid, setIsValid] = useState(false);

  const validate = useCallback(async () => {
    try {
      await schema.validate(values, { abortEarly: false });
      setErrors({});
      setIsValid(true);
    } catch (e) {
      if (e instanceof ValidationError) {
        const errors = {};
        e.inner.forEach((key) => {
          errors[key.path] = key.message;
        });
        setErrors(errors);
        setIsValid(false);
      }
    }
  }, [schema, values]);

  useEffect(() => {
    validate();
  }, [validate]);

  return { errors, isValid };
}

export default useValidation;

Code explanation

In this code I use useEffect to keep the errors object and isValid property, by default isValid should be false, because when we start our form we don't have any values.

Added a function named validate, this function should receive the form values and pass this value to object validation. If the form state has a valid value, we set an empty object in the errors state and true in isValid property, but if it has any error, we need to know if is an error of validation (ValidationError instance), before setting them in the errors state and false in isValid.
To update the errors every time that form is changed, we pass the form state as a dependency in the useEffect hook.
Added object error with the specific property in every field.

I use useCallback with validate function and pass this function as a useEffect dependency.

Finally, I return an object with the form errors and one property that shows me if the form is valid or not.

Last change

Now we need to make just two small changes in DebounceForm component:

The first change is to adjust the object returned by useValidation, now we want to know if the form is valid, so we just need to take this property.

const { errors, isValid } = useValidation(form, FormValidations);

The second small change is to use isValid in the submit button.

<div className="form-group">
  <button
    type="button"
    className="btn btn- 
    primary"
    disabled={!isValid}
  >
    Submit
  </button>
</div>

Comparing the forms

DebounceForm results:
img

ControlledForm results:

In the first example, we have 3 renders, and in the second we have 13 renders, it's a big difference.

I'm not saying that this is the better approach, in many cases, this will not make any sense, so you should discover for yourself what is best for your application.
I hope that this post helped you to figure that out!

18