React Hook Form: a unique implementation
Tags: react, javascript, tutorial, form
Published: 2022-11-16 20:48:06

**Forms** are used everyday, to login/signup, fill information when ordering something, ... It is really a masterpiece of a site.

I started making form in **React** with `Redux Form` which uses `Redux` to store information about forms. Yep, it was the old time where we were using `Redux` for everything.

Nowadays, things have changed. We have multiple libraries: `Formik`, `React Final Form`, `React Hook Form`, ... that most of the time uses **React state** to store information.

I know that some framework like **Remix**, encourages us to use pure html to make forms. But often, we have to use a client library if we want a nice **user experience** with quick feedback, or when you want complex validations on fields depending to each others.

**React hook form** is a library focusing on performance. Looking at its implementation is really interesting to learn some pattern that can can be used in other cases.
Let's look at what makes it unique compared to other form libraries implementations.

----

## Prerequisites

Before starting to talk about implementation, I want to define some terms to be all on the same page:
- **Field**: the element that collects the data from the user (input, select, datepicket, ...).
- **Field name**: the identifier of the field.
- **Field value**: the value filled by the user.

----

## What would be a "simple" implementation?

If today, I have to make a form implementation. Instinctively, I would make one like `Formik` or `React Final Form` using **state**:

```js
function MyForm() {
  const [values, setValues] = useState({
    firstname: "",
    lastname: "",
  });
  const onChange = (fieldName, fieldValue) => {
    setValues((prevValues) => ({
      ...prevValues,
      [fieldName]: fieldValue,
    }));
  };

  return (
    <form
      onSubmit={() => {
        // Do something with the form values
        // that are in the `values` variable
      }}
    >
      <label>
        Firstname
        <input
          type="text"
          name="firstname"
          value={values["firstname"]}
          onChange={(e) => onChange(e.target.value)}
        />
      </label>
      <label>
        Lastname
        <input
          type="text"
          name="lastname"
          value={values["lastname"]}
          onChange={(e) => onChange(e.target.value)}
        />
      </label>
      <button type="submit">Submit</button>
    </form>
  );
}
```

Nothing fancy here. I just store the value filled by the user in a React state. And here we go.
It's a really simplified implementation. In a real life, I would probably use a **reducer** because I want to store more than values: validation errors, know if the form is submitting, if fields are dirty, ...

{% details If you want to see a more realistic implementation %}
```js
// I do not handle validation and form states
// but if I do I will probably use a reducer to that 
// instead of multiple states
function useForm(initialValues = {}) {
  const [values, setValues] = useState(initialValues);

  const handleSubmit = (onSubmit) => (e) => {
    e.preventDefault();

    onSubmit(values);
  };

  const register = (fieldName) => {
    return {
      value: values[fieldName],
      onChange: (event) => {
        setValues((prevValues) => ({
          ...prevValues,
          [fieldName]: fieldValue,
        }));
      },
    };
  };

  return {
    register,
    handleSubmit,
  };
}

function MyForm() {
  const { values, onChange, handleSubmit } = useForm({
    firstname: "",
    lastname: "",
  });

  return (
    <form
      onSubmit={() => {
        // Do something with the form values
        // that are in the `values` variable
      }}
    >
      <label>
        Firstname
        <input
          type="text"
          name="firstname"
          value={values["firstname"]}
          onChange={(e) => onChange(e.target.value)}
        />
      </label>
      <label>
        Lastname
        <input
          type="text"
          name="lastname"
          value={values["lastname"]}
          onChange={(e) => onChange(e.target.value)}
        />
      </label>
      <button type="submit">Submit</button>
    </form>
  );
}
```
{% enddetails %}

And you know what? That's not the way **React Hook Form** is implemented.

---

## Key points of React Hook Form implementation

### Do not use React state

The main things to know is that the library does not use React state / reducer to store the data but references.
It uses lazy initialization of **React ref**:

```js
function useForm(config) {
  const formControl = useRef(undefined);

  // Lazy initialization of the React ref
  // Enter the condition only at the first render
  // (`createFormControl` returns an object
  if (formControl.current === undefined) {
    formControl.current = createFormControl(config);
  }
}
```

And then in the `createFormControl` everything is stored in `const` that are mutated:

```js
function createFormControl({ initialValues }) {
  const formValues = initialValues;

  const onChange = (fieldName, fieldValue) => {
    formValues[fieldName] = fieldValue;
  };

  return {
    onChange,
  };
}
```

And now, it's blazingly fast because no more render. 

> Mmmm wait, no more render? How can we know when values are changing and state of form?

Let's see it.

----

### Observer pattern

This pattern is really used in the industry: `react-query`, `react-redux`, ... uses it.

The principle is really simple but so powerful.
We have:
- a `subject`: it's an object that keep track of an entity changes and notify of this change
- `observers`: they listen to the entity changes by subscribing to the subject

{% details If you want to see an implementation %}

```js
function createSubject() {
  const listeners = [];

  const subscribe = (listener) => {
    // Add the listener
    listeners.push(listener);

    // Return an unsubscribe method
    return () => {
      listeners = listeners.filter((l) => l !== listener);
    };
  };

  const update = (value) => {
    for (const listener of listeners) {
      listener(value);
    }
  };

  return {
    subscribe,
    update,
  };
}
```

{% enddetails %}

**React Hook Form** has 3 subjects:
- `watch`: to track changes of field values
- `array`: to track changes of field array values
- `state`: to track changes of the form state

And now, the `useWatch` hook subscribe to the `watch` subject and update a `React state` when its the field that we want to track that has changed.

And here we go our component when needed.


> Wait! When I want to be notified when the form is going dirty, my component does not re-render when other state values changes. How is it possible?

That's the next key point.

----

### Proxies / defineProperty

If you don't know what is a proxy you can read my article [Proxy in JS: what the hell?](https://dev.to/romaintrotard/proxy-in-js-what-the-hell-19d0).

In RHF, proxies are used to know which properties of the state are used in components.

Thanks to them, we can know which properties are listened by the component and only render it when these properties are changing.

```js
function createProxy(formState, listenedStateProps) {
  const result = {};

  // Loop on the property which are in the form state
  for (const propertyName in formState) {
    Object.defineProperty(result, {
      get() {
        // Keep in mind that the property is listened
        listenedStateProps[propertyName] = true;

        // And returns the actual value
        return formState[propertyName];
      },
    });
  }

  return result;
}
```

And thanks to that and the **observer pattern** we can update the component when listened form state properties are changed.

```js
// control is an object that has all the logic
// and the mutated object like `_formValues`,
// `_formState`, `_subjects`, ...
function useFormState(control) {
  // At start nothing is listened
  // In reality there are more properties
  const listenedStateProps = useRef({
    isDirty: false,
    isValid: false,
  });
  // Initialize with the current `_formState` which is
  // mutated
  const [formState, setFormState] = useState(
    control._formState
  );

  useEffect(() => {
    return control._subjects.state.subscribe(
      ([stateProp, stateValue]) => {
        // If the changed property is listened let's update
        if (listenedStateProps.current[stateProp]) {
          setState((prev) => ({
            ...prev,
            [stateProp]: stateValue,
          }));
        }
      }
    );
  }, [control._subjects]);

  return createProxy(formState, listenedStateProps);
}
```

----

### Stable event listener with no stale external data

Another strategy, is the usage of reference for values used in event listener that are memoized thanks to `useCallback` or used in `useEffect`.

**Why?** 
Because we don't want to have stale data in our callback so we would have to add it in the dependency of `useCallback`. Because of that, it will create a brand new reference everytime the dependency is changing that does not make sense because being an event listener.

> **Note:** it actually create a new reference at each render but the one returned by `useCallback` will be always the same.

Instead of that:

```js
function MyComponent({ someData }) {
  // The reference of showData is not stable!
  const showData = useCallback(() => {
    console.log("The data is", someData);
  }, [someData]);

  return (
    <MemoizedButton type="button" onClick={showData}>
      Show the data, please
    </MemoizedButton>
  );
}
```

We have that:

```js
function MyComponent({ someData }) {
  const someDataRef = useRef(someData);

  useLayoutEffect(() => {
    // Keep the reference up-to-date
    someDataRef.current = someData;
  });

  // The reference of showData is now stable!
  const showData = useCallback(() => {
    console.log("The data is", someDataRef.current);
  }, []);

  return (
    <MemoizedButton type="button" onClick={showData}>
      Show the data, please
    </MemoizedButton>
  );
}
```

If you have already my article [useEvent: the new upcoming hook?](https://dev.to/romaintrotard/useevent-the-new-upcoming-hook-3482), you probably have noticed that it's the same principle. Unfortunately, `useEvent` will not come soon so we would have to do that a little bit longer in our projects.

{% details Complementary informations %}

In reality, in the **React Hook Form** codebase the implementation is not the same.

The ref is updated directly in the render, but I would not recommend you to it because can cause some trouble with new concurrent features and have inconsistency in your components.

```js
function MyComponent({ someData }) {
  const someDataRef = useRef(someData);

  // Do not update directly in the render!!!
  someDataRef.current = someData;

  // But use a `useLayoutEffect`
  useLayoutEffect(() => {
    someDataRef.current = someData;
  });
}
```

That's the same pattern than the so wanted [`useEvent` hook](https://dev.to/romaintrotard/useevent-the-new-upcoming-hook-3482) that will finally not to out :(

{% enddetails %}

----

## Conclusion

You should be more comfortable to browse the **React Hook Form** and understand the code.
Some of the key points can be used in your own codebase or if you want to develop a library.

Watch out not to too optimize your code. If you want to apply the same pattern with mutation, I recommend to mutate it everytime the data is changing and not to try to not mutate when you think it's not necessary because it can cause you some trouble when your component renders conditionally. For example I would prevent this [kind of code](https://github.com/react-hook-form/react-hook-form/blob/c69e9b9678877ab532660cc08c31d236591a3664/src/logic/createFormControl.ts#L284) which only mutates form state if we use the `formState.isDirty` on the current render, but will not work when you listen the form state dirty at the next render.

{% codesandbox react-hook-form-dirty-pb-bwxvjr %}

----

Do not hesitate to comment and if you want to see more, you can follow me on [Twitch](https://twitch.tv/rainbowapps ) or go to my [Website](https://www.romaintrotard.com). And here is a little link if [you want to buy me a coffee](https://ko-fi.com/romaintrotard) :coffee: