When we working with form validation, most of us would be familiar with libraries such as Formik and Redux-form. Both are popular among the community and built with Controlled Components.

What is Controlled Component?

React is driving the internal state of itself. Each input interaction or change will trigger the React's Component life cycle. The benefit of having that is:

Every state mutation will have an associated handler function. This makes it straightforward to modify or validate user input.

This feature is great for handling forms validation. However, there is a hidden cost. If you run the following code and pay attention to the developer console;



function Test () { const [ numberOfGuests , setNumberOfGuests ] = useState (); console . log ( ' rendering... ' ); return ( < form onSubmit = { () => console . log ( numberOfGuests ) } > < input name = "numberOfGuests" value = { numberOfGuests } onChange = { setNumberOfGuests } /> </ form > ); }

You should see console.log repeating 'rendering...' in the dev console each time as you type. Obviously, the form is getting re-rendered each time. I guess with simple use case it wouldn't cause much of issue. Let's try to implement something which is more close to a real-world example.



function Test () { const [ numberOfGuests , setNumberOfGuests ] = useState (); expensiveCalculation ( numberOfGuests ); // Will block thread console . log ( ' rendering... ' ); return ( < form onSubmit = { () => console . log ( numberOfGuests ) } > < input name = "numberOfGuests" value = { numberOfGuests } onChange = { setNumberOfGuests } /> </ form > ); }

It's pretty much the same code, except this time each render will execute an expensive function before render. (let's assume it will do some heavy calculation and blocking the main thread) hmmm... now we have an issue because user interaction can be potentially interrupted by that. As a matter of fact, this type of scenario did give me a headache in terms of form performance optimization.

Solution

Off course there are solutions on the table, you can use a memorize function to prevent execute the function on each render. An example below:



function Test () { const [ numberOfGuests , setNumberOfGuests ] = useState (); // The following function will be memoried with argument and avoid recalculation const memoizedValue = useMemo (() => computeExpensiveValue ( numberOfGuests ), [ numberOfGuests ]); return ( < form onSubmit = { () => console . log ( numberOfGuests ) } > < input name = "numberOfGuests" value = { numberOfGuests } onChange = { setNumberOfGuests } /> </ form > ); }

However, we actually have another option to skip re-render the form when user typing.

Uncontrolled Components

What's Uncontrolled Component?

In most cases, we recommend using controlled components to implement forms. In a controlled component, form data is handled by a React component. The alternative is uncontrolled components, where form data is handled by the DOM itself.

This means if you are going to build uncontrolled form and you will be working on methods to handle the DOM and form interaction. Let's try an example with that then.



function Test () { const numberOfGuests = useRef (); expensiveCalculation ( this . state . numberOfGuests ); return ( < form onSubmit = { () => console . log ( numberOfGuests . current . value ) } > < input name = "numberOfGuests" ref = { numberOfGuests } value = { numberOfGuests } /> </ form > ); }

By leveraging uncontrolled component, we exposed with the following benefits:

User interaction no longer triggers re-render on change. Potential less code to write. Access to input's ref gives you the power to do extra things, such as focusing on an error field.

I guess one quick question will pop up in your head, what if I want to listen for input change? Well now you are the driver of the inputs, you can handle that by native DOM event. (it's all just javascript) example below:



function Test () { const numberOfGuests = useRef (); const handleChange = ( e ) => console . log ( e . target . value ) useEffect (() => { numberOfGuests . current . addEventListener ( ' input ' , handleChange ); return () => numberOfGuests . current . removeEventListner ( ' input ' , handleChange ); }) return ( < form onSubmit = { () => console . log ( numberOfGuests . current ) } > < input name = "numberOfGuests" ref = { numberOfGuests } /> </ form > ); }

At this point, we are writing more code than Controlled Component. But what if we can build a custom hook to handle all of that and re-use the same logic throughout multiple forms within the app.

Hooks

Check out the example below; a custom form validation hook:



import useForm from ' react-hook-form ' ; function App () { const { register , handleSubmit } = useForm (); const onSubmit = ( data ) => { console . log ( data ) }; return ( < form onSubmit = { handleSubmit ( onSubmit ) } > < input name = "numberOfGuests" ref = { register ({ required : true }) } /> </ form > ) }

As you can see from above, the implementation is clean and simple. There is no render-props wrap around the form, no external components to wrap around individual fields and validation rules are centralized too.

Conclusion

The uncontrolled component can be a better performance neat and clean approach and potentially write a lot less code and better performance. If you find above custom hook example interest and like the syntax. You can find the Github repo and docs link below:

Github: https://github.com/bluebill1049/react-hook-form

Website: https://react-hook-form.com

☕️ Thanks for reading.