CyberSEO Pro plugin for WordPress

React Hook Form: a unique implementation

Read Time:8 Minute, 19 Second

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:

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>
 );
}
Enter fullscreen mode Exit fullscreen mode

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>
 );
}
Enter fullscreen mode Exit fullscreen mode

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:

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); }
}
Enter fullscreen mode Exit fullscreen mode

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, };
}
Enter fullscreen mode Exit fullscreen mode

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
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, };
}
Enter fullscreen mode Exit fullscreen mode

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

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;
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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:

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>
 );
}
Enter fullscreen mode Exit fullscreen mode

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>
 );
}
Enter fullscreen mode Exit fullscreen mode

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; });
}
Enter fullscreen mode Exit fullscreen mode

That’s the same pattern than the so wanted useEvent hook that will finally not to out 🙁


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


Do not hesitate to comment and if you want to see more, you can follow me on Twitch or go to my Website. And here is a little link if you want to buy me a coffee

Source: https://dev.to/romaintrotard/react-hook-form-a-unique-implementation-3oll

Django & Docker – SQLite, MySql, and PostgreSQL samples Previous post Django & Docker – SQLite, MySql, and PostgreSQL samples
DigitalOcean Welcomes Cloudways to the Family Next post DigitalOcean Welcomes Cloudways to the Family

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.