Go to main content
November 16, 2022
Cover image

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.


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.

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

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, …

If you want to see a more realistic implementation
// 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>
);
}


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


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:

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:

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.


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
If you want to see an implementation
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,
  };
}

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.


If you don’t know what is a proxy you can read my article Proxy in JS: what the hell? .

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.

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.

// 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);
}

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:

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:

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? , 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.

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.

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 that will finally not to out :(


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 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.


You can find me on Twitter if you want to comment this post or just contact me. Feel free to buy me a coffee if you like the content and encourage me.