Skip to content

How to create a basic implementation of React Hook Form

Posted on:November 9, 2022 at 07:07 AM

Introduction

I was introduced to React Hook Forms a couple of weeks ago by a coworker as he was explaining to me how they are using it in their team. My first thought was Who uses a library just for managing forms?, up until now I’ve just used useState or similar solutions to store the form state and I was fine with the results.

But then I started checking their documentation and found an interesting thing, when you changed an input field it doesn’t re-render the form…

React Hook Form In comparison this is a normal one:

Normal controlled form How are they managing to do so? I wondered… So I started checking their code and this is how it works:

Why does a normal form gets re-rendered when you type a new value

First let’s see a normal form, one that uses useState to store the value

function MyForm() {
    const [firstName, setFirstName] = useState("")

    return <form>
        <input name="firstName" value={firstName} onChange={(e) => setFirstName(e.target.value)} />
    </form>
}

Every time you type something you trigger a function that changes the value of firstName and thus trigger a re-render. The thing is that you don’t need in most cases to store this value right away because this value is anyway stored in the HTML.

What is the React Hook Form approach?

React exports many hooks to us, like useState and useRef, the latest is usually popular to hold references to DOM elements, but it’s more than that, you can think of the useRef hook as a way to have instance variables in a React component, this means that you can have the same value preserved across re-rendering and changing it won’t trigger a re-render.

In that order of ideas, in most cases, it makes sense to store the values of a form in a ref and this is basically what React Hook Forms do.

Creating a simple replica of React hook forms

To illustrate better this idea, let’s create a basic implementation of React Hook Forms, let’s call it React Mini Forms 😛

Creating a useForm hook

The first thing we will need is to create a normal function that will be used as a hook. This function will return the following properties:

We will store the form values in a ref because as we said earlier, every time we change the value this won’t retrigger a render, inside the useForm hook, let’s put

// useForm.js
import { useRef, useState } from 'react'

export function useForm() {
    const state = useRef()
  const [_, setFormState] = useState({ renderCount: 0 }) // Will explain later

    // This will be the ref storing the current values of the fields
    if(!state.current) {
        state.current= {
            fields: new Map(),
            watching: new Map(),
        }
    }

    return {
        register: function(fieldName) {
        },
        handleSubmit: function() {
        },
        watch: function(fieldName) {
        }
    }
}

Consuming the hook

Now in our React Component, we will use the hook in the following way

import { useForm } from './useForm'

function App() {
    const { register, handleSubmit, watch } = useForm()

    console.log("The current value of the lastName is: ", watch("lastName"))

    const onSubmit = (formData) => {
        console.log("Data: ", formData)
    }

    return <form onSubmit={handleSubmit(onSubmit)}>
        <input {...register("firstName")} />
        <input {...register("lastName")} />

        <button>Submit</button>
    </form>
}

Implementing the register function

The register function as we said earlier will be the one in charge of adding new fields to the form state In this function, we will add the field to the list of fields of the form (If it’s not already there) and we will return all the normal attributes of inputs like name, onChange, etc.

register: (name) => {
  if (!state.current.fields.has(name))
    state.current.fields.set(name)

  return {
    name,
    onChange: (event) => {
      state.current.fields.set(
        name,
        event.target.value,
      )
    }
  }
},

Implementing the watch function

This function was the hardest for me to understand, so here’s a more basic implementation that works. We have a list of fields that the user is currently “watching”. Every time it gets called it adds the field to the watching list if it’s not already there and it returns the current value of the field

watch: (name) => {
  if (!state.current.watching.has(name)) {
    state.current.watching.set(name, true)
  }

  return state.current.fields.get(name)
},

Now we need to modify the register function so every time a value changes and the field is on the watch list, we will purposely call a setState to tell React that we need to re-render the component

register: (name) => {
    // ....

  return {
      // ...
    onChange: (event) => {
    if (state.current.watching.has(name)) {
        setFormState(state => ({
          renderCount: state.renderCount,
        }))
      }

    }
  }
},

Implementing the handleSubmit function

This is the easier function, it just returns the current value of all fields

handleSubmit: callback => e => {
  e.preventDefault()
  callback(state.current.fields)
},

The final code

All this code should be enough to have a working solution but remember that this is a basic implementation to demonstrate the idea, thus it doesn’t have performance improvements and probably it doesn’t handle all edge cases.

One thing to take into account when using this approach is that this could be more performance in a complex component as long as you're not subscribed to all the fields, if you are subscribed to all the fields a new render will happen every time anyway so a simpler solution would be better.

Let me know any comments you might have!

Here’s the final code See it on Codesanbox 📦

import { useRef, useState } from "react";

export function useForm() {
  const state = useRef();
  const [_, setFormState] = useState({ renderCount: 0 });

  // This will be the ref storing the current values of the fields
  if (!state.current) {
    state.current = {
      fields: new Map(),
      watching: new Map()
    };
  }

  return {
    register: (name) => {
      if (!state.current.fields.has(name)) state.current.fields.set(name);

      return {
        name,
        onChange: (event) => {
          if (state.current.watching.has(name)) {
            setFormState((state) => ({
              renderCount: state.renderCount
            }));
          }

          state.current.fields.set(name, event.target.value);
        }
      };
    },
    watch: (name) => {
      if (!state.current.watching.has(name)) {
        state.current.watching.set(name, true);
      }

      return state.current.fields.get(name);
    },
    handleSubmit: (callback) => (e) => {
      e.preventDefault();
      callback(state.current.fields);
    }
  };
}