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.
Top comments (2)
Hi Bill, as far as I know you can still attach the "onChange", "onBlur", etc.. to uncontrolled component, no need to use plain events. The key is not to use the "value" prop, so the DOM component can rely on its internal state.
Also, I'm facing with a React bug, where controlled components loose the cursor position: using the uncontrolled component is a good way to avoid this bug, but then I'm not sure how to validate the input: do you have any suggestion?
Have you tried with this custom hook which I have built? react-hook-form.com/ it may have answer to your question.